Automation Center Reference Public

Research & Known Gaps

The verification record behind this guide: which source files were read, the milestone evidence including the byte-identical Observe inertness proof and the capped 25-of-120 live run, everything M4 and M5 have not shipped, the declared-but-unread config keys, and all ten named debt items with their owners.

Guide version: r1 Module version: 0.3.3 Updated: 2026-09-04 Estimated time: 23 min 1 views
Reference

Research & Known Gaps

How we know what this guide claims, what is measurably true today, and — in the same amount of detail — what is not true yet. A reader who trusts the rest of this guide is entitled to know exactly where its ground stops.

ℹ️
Why this page exists. An undocumented deliberate compromise is indistinguishable from a bug, and gets "fixed" by the next reader — usually at some cost. Everything on this page is either evidence or a named compromise with an owner. Nothing here is a wish list.

How this guide was verified

Every behavioural claim in this guide was read out of the working tree, not recalled from a specification. Where the specification and the code disagreed, the code won and the specification was corrected — that happened at least four times (see Decisions). The files below are the primary sources.

The module itself

PathWhat it settles
config/automations.phpThe 71-line sidecar contract docblock — the only normative description of what a type may declare. Its own array is empty by design: the module registers no types.
libraries/Automation_registry.phpDiscovery, path containment, checksum composition, the is_available() gate.
libraries/Automation_executor.phpMAX_CHUNKS_PER_TICK 20, FIRST_LIVE_RUN_LIMIT 25, the 600-second lease, publisher revalidation, the run-level failure codes.
libraries/Automation_rule_engine.phpThe operator vocabulary and evaluation order.
libraries/contracts/*.phpBoth interfaces — and the fact that their @param tags say object $context, not a class (see Phantom APIs).
jobs/AutomationDispatchJob.phpautomation_dispatch, * * * * *, runsForGlobalScope() === false, a 45-second budget, 100 claims per tick, Phase A / Phase B split.
controllers/Automations.phpThe gates, _live_refusal()'s four preconditions, and the two hardcoded version columns in save().
helpers/automations_helper.phpautomations_reason_codes(), scope-token normalisation, config hashing.
config/{permissions,routes,menu,default_menu,system_logs}.phpThe ten frozen permission keys, the Administration placement, the registered log events.
migrations/*.php (3)The three tenant tables, the toggle split, and automation_runs.run_mode.
views/automations/**The five lazy tabs, the wizard, the run-detail modal — and the builder's hardcoded Action/Safety panels.

Outside the module

PathWhat it settles
modules/HumanResource/config/automations.phpThe only registered type today, attendance.auto_decision — and every declared-but-unread key on this page came from reading it against its consumers.
modules/HumanResource/automation/Attendance_auto_decision_handler.phpThe real reason codes, the overtime policy, the eligibility rules, and the deliberate re-implementation named in debt item 20.1.
modules/HumanResource/automation/Attendance_condition_provider.phpThe condition catalogue the wizard offers.
modules/HumanResource/automation/Attendance_transition_gateway.phpThe headless host shim — debt items 20.9 and 20.10.
application/migrations/20260903232432_seed_system_actor_user.phpThe System actor, and the id-0 poisoning guard added after a real tenant produced one.
modules/Automations/specs/automation-center-implementation-plan.md20 sections. §14 (reason-code drift) and §20 (known debt) are the sources for two whole sections below.
modules/Automations/specs/automation-center-status.mdThe milestone evidence tables reproduced under Milestone evidence.
.claude/rules/automation-center.mdThe agent-facing invariants, kept in step with the above.
????
Two verification styles, and the difference matters. A new capability is proven by making it happen and then proving nothing else happened. A behaviour-preserving refactor is proven by evidence that nothing changed — identical test counts, identical failures. Milestone 3 step 1 was the second kind, and it re-ran a sibling module's failing suite with the working tree stashed to prove its four failures were pre-existing.

Milestone evidence

#MilestoneStatusVersion
M1Foundation — scaffold, migrations, toggle split, registry, dispatch job, modelsDone 2026-09-03Automations 0.1.x
M2Builder + Observe modeDone 2026-09-030.2.0 / HR 1.21.0
M3Live decisions, safeguards, cloneDone 2026-09-030.3.0 / HR 1.23.0
M4Operations UI, analytics, notifications, both API surfacesPending
M5Hardening — docs, OG image, behavioural tests, pilot rolloutPending

M1 — the dispatcher does not double-fire

Run against the dev primary database dev_app with a throwaway, self-cleaning harness. The point of M1 was never "it runs" — it was idempotency, because a scheduler that opens two runs for one occurrence would later approve the same record twice.

13/13
dispatcher end-to-end
21
module tests, 57 assertions
944
core tests, 3,506 assertions
0
duplicate runs on re-tick

Tick 1 opened exactly one run, advanced next_run_at_utc, wrote the 64-char idempotency key, snapshotted the configuration and stamped a correlation id. Re-presenting the same occurrence on tick 2 created no second run. An unregistered type closed its run failed / DEFINITION_UNAVAILABLE and moved the automation to needs_review rather than retrying forever. Every test row was removed and the module toggle restored.

What M1 did not cover. The harness ran on the primary connection, not a tenant database. The per-tenant fan-out is the cron runtime's behaviour rather than this module's — but it was still deliberately confirmed on a tenant before M3 allowed any mutation, which is what M2 and M3 below did.

M2 — the inertness proof

Observe mode's entire value is that it changes nothing. "We looked and it seemed fine" is not evidence of that, so the M2 run on tenant dev_c_rs8 fingerprinted the database before and after:

Measured before and after a full Observe runResult
Attendance row countbyte-identical
CRC fingerprint over status / checked_by / checked_atbyte-identical
Pending countbyte-identical
attendance_metrics row countbyte-identical

Alongside that: 356 candidates evaluated, matching an independent SQL count exactly; every item carried an outcome and a reason code; the counts reconciled (would + skipped = candidates); Observe results landed in would_count and never in approved_count; exactly one audit row was written for the run. Test suites: 41 module / 230 HumanResource / 955 core. A browser pass over the UI scored 27/27 — one fragment fetched on load, unvisited tabs empty, tab switching not navigating, re-selection not refetching, both server-side DataTables initialising, no stuck-light panel in dark mode, zero JS errors.

ℹ️
Two real bugs came out of that verification, both of the kind only running the thing finds. The contract interfaces were not loaded before a module's handler was required — PHP resolves implements at compile time, so that was an immediate fatal, not a subtle later miss. And a single broken type took down the whole tick instead of just its own run.

M3 — the cap held, and the rollback was clean

The Live run was driven through the real cron CLI against real attendance on tenant dev_c_rs8. The headline number is the one to internalise:

120
pending in the window
25
approved — the cap holding
26
audit rows (1 run + 25 records)
0
records changed outside the set

Every one of the 25 came out status=approved, with checked_by set to the System actor, checked_at stamped, and carrying the attendance_metrics row payroll reads. Each per-record audit entry was remarked automation; the run recorded itself run_mode=live. No record outside the approved 25 changed. Rollback restored all 120 rows byte-identical, leaving zero system-actor approvals, zero leftover automations, zero orphan metrics rows and zero leftover audit rows. Clone was verified separately at 15/15 in the browser and 28/28 in the database, against a source deliberately left published, live, scheduled and on revision 7 with run pointers — every reset checked, the source itself unmodified.

The System actor was seeded on three databases and confirmed user_type=system, status=inactive, disable_login=1, and invisible to every staff-scoped query. Suites at M3: 968 core / 255 HumanResource / 61 Automations.

Four defects surfaced only because a live run was actually driven. One tenant's users.id had lost its PRIMARY KEY and AUTO_INCREMENT (pre-existing drift), so the actor insert produced id 0 — precisely the blank-approver state the actor exists to prevent — and the migration's naive "a row exists" check would have skipped past it forever. __get forwards properties, not method calls, so var_biometrics_option() had to be declared explicitly on the gateway. The handler read $context->title, which does not exist (the executor sets automation_title) — the approval would still have happened and only the audit remark would have been blank, which is noticed much later or never. And the executor never revalidated the publisher although permissions.php documented that it did.

What is not shipped

Milestones 4 and 5 are pending in full. Nothing below exists in the working tree; do not design against it, and do not report its absence as a bug.

WorkstreamNot shippedWhat exists today instead
M4.1 Overview Dispatcher health strip (last heartbeat, due backlog, next scan, timezone) and the Needs Attention list. Status counts, the five most recent runs, and the registered type list.
M4.2 Analytics Automation_runs_model::get_analytics_totals(), the run-reliability vs record-outcome split, filters, the per-automation table. tabs/analytics.php sums the last 200 run rows in a PHP foreach — debt item 20.6.
M4.3 Settings An editable form for module-owned defaults, server-side range validation, the sticky save bar, and anything emitting settings:automation. A read-only diagnostics tab — while automations_settings and the registered settings:automation log event both already exist. That incoherence is acknowledged, not accidental.
M4.4 Notifications config/notifications.php entirely — no automation_run_failed, no automation_needs_review, no automation_publisher_revoked. Nothing. A failed run is visible in the Runs tab and nowhere else.
M4.5 APIs api_routes.php, Automations_api, api_scope.php (automations:read), euapi_routes.php, Automations_euapi, regenerated OpenAPI, AutomationsApiTest. Nothing. There is no API surface at all — neither integration nor end-user.
M4.6 States The designed empty / engine / run / permission / conflict state catalogue. Ad-hoc states per view.
M5 Hardening The OG image public/assets/images/modules/automations-og.png (referenced by metadata.og_image, file does not exist); behavioural tests to complement the source-text ones; the dark-mode and a11y pass; the staged pilot rollout. Source-text contract tests, and this guide.

Two deliberate deferrals, distinct from "pending": the dashboard widget pair (config/widgets.php + helpers/automations_widget_helper.php) is a natural MINOR follow-up, and the automation_daily_metrics rollup table will be built only on a measured need — deriving from run aggregates satisfies the single-source-of-truth rule until a measurement says otherwise.


Phantom APIs — declared, documented, and read by nothing

This section is the one most likely to save you a wasted afternoon. Each item below looks like a supported extension point. None of them is wired to anything.

There is no Automation_context class

The handler interface's docblocks say @param object $context Automation_context. That name refers to no class anywhere in the tree. The first specification draft declared typed parameters — preview(Automation_context $context, …) — and following it produced a fatal, because the executor passes a plain stdClass-style object it assembles itself. The type hint was removed; the docblock name survives as a label for the shape.

// modules/Automations/libraries/contracts/Automation_handler_interface.php
/**
 * @param object $context Automation_context      <-- a NAME, not a class
 */
public function preview($context, array $config);

Read the properties the executor actually sets before you use one. The M3 verification caught a handler reading $context->title, which does not exist — the executor sets automation_title. Nothing errored; the audit remark would simply have been blank forever.

Nothing reads publish_permissions

The HumanResource sidecar declares it, and its comment describes exactly the behaviour a reader would want:

// modules/HumanResource/config/automations.php
// Extra permissions the PUBLISHER must hold to publish a LIVE automation of this
// type; automations_publish is always required on top. Revalidated at run time,
// so a lapsed permission pauses the automation rather than continuing on the
// publisher's behalf.
'publish_permissions' => array(
    'approve' => array('attendance_approval'),
    'reject'  => array('attendance_approval', 'attendance_update'),
),

A repository-wide grep finds the key in exactly two files: the sidecar that declares it and the contract docblock that documents it. No consumer. The consequence is concrete and worth stating plainly:

An operator holding automations_publish and automations_go_live but not attendance_approval can today publish a LIVE attendance automation. The four _live_refusal() preconditions still apply and the run-time publisher revalidation still runs — but it revalidates automations_go_live only, never the type's declared extra permissions. Treat the type-level permission as documentation of intent, and gate access with the module permissions until this is wired.

Nothing applies defaults

defaults is the richest, best-commented block in the HumanResource sidecar — action, run_mode, overlap_policy, lookback_days: 7, settle_hours: 12, overtime_policy: skip, each with the measurement or the reasoning behind it. It is read by nothing. The same defaults are re-hardcoded in the builder view (views/automations/builder_modal.php writes 7 and 12 as literal value attributes and 'skip' as the dropdown's selected option). The numbers agree today; nothing keeps them agreeing.

Four of five capabilities keys are unread

KeyRead byEffect
liveAutomations::_live_refusal()Enforced — precondition 2 of 4.
previewnothingDecorative
dry_runnothingDecorative
chunkednothingDecorative
manual_runnothingDecorative

Declaring 'preview' => false will not stop the builder previewing; declaring 'chunked' => false will not stop the executor chunking. The capability contract is one key wide.

The two version columns are hardcoded on save

// modules/Automations/controllers/Automations.php  (save)
"definition_version"    => 1,
"config_schema_version" => 1,

Both columns exist to detect that a stored automation was authored against an older definition. The registry does read the sidecar's own definition_version / config_schema_version when composing its checksum — but the automation row is stamped 1 unconditionally, so a comparison between the two would be meaningless. Clone copies the source row's values faithfully, which is correct and also inherits the 1. Do not build a migration path on these columns until save() reads them from the definition.

The reason-code vocabulary validates nothing, and has drifted

automations_reason_codes() in the module helper returns 17 codes. Reason codes actually arrive from three sources, and the declared list matches none of them cleanly, because record_item() validates outcome but writes reason_code verbatim.

SourceCodes it really emits
The handler's baseline rules ELIGIBLE · PAYROLL_LOCKED · SCHEDULE_CHANGE_PENDING · NOT_YET_SETTLED · OVERTIME_PRESENT · RULE_NOT_MET · APPROVED · TRANSITION_ERROR
Passed through from the transition service, tagged refused_by = 'transition_service' NOT_FOUND · PERMISSION_DENIED · PAYROLL_LOCKED · ATTENDANCE_WINDOW_LOCKED · OVERLAP_CONFLICT · SAVE_FAILED · OK
The executor, for run-level failures DEFINITION_UNAVAILABLE · CONFIG_NEEDS_REVIEW · PUBLISHER_UNAUTHORIZED · EXECUTOR_ERROR · AUTOMATION_MISSING · HANDLER_ERROR · HANDLER_CONSTRUCT_FAILED

Five codes the handler emits every run — APPROVED, NOT_YET_SETTLED, OVERTIME_PRESENT, RULE_NOT_MET, TRANSITION_ERROR — are absent from the declared list. Nine declared codes are emitted by nobody: MISSING_TIME_IN, MISSING_TIME_OUT, OVERLAPPING_ATTENDANCE, ATTENDANCE_ANOMALY, LEAVE_CONFLICT, OUTSIDE_SCOPE, STATUS_CHANGED, ALREADY_SATISFIED, MUTATION_VALIDATION_FAILED. The list has exactly one consumer — a test asserting the vocabulary reflects features that exist. Harmless at runtime today; it will produce wrong labels the moment any UI renders from it. That is debt item 20.8.

The builder is hardcoded to one type

The wizard's Action and Safety panels render lookback_days, settle_hours, overlap_policy and overtime_policy as literal fields with literal defaults and literal bounds (min="1" max="365", min="0" max="720"). Those are the attendance type's fields. The rest of the module is registry-driven; this view is not. A second registered type needs these panels made registry-driven first — otherwise it either inherits attendance's configuration shape or gets no configuration UI at all.


The known-debt register

Plan §20. Each item is verified, each has an owner, and each is a deliberate compromise — not an oversight. Fixing one without reading its rationale is how a safe system becomes an unsafe one.

ℹ️
The register is now ten items, not seven. The status document's own change log still records "new §20 — known debt (7 items)" and a later "newly-found eighth" — §20 has since grown to 20.1–20.10. Trust §20, and treat that count as a small piece of documentation drift in its own right.
20.1 — There is no side-effect-free eligibility predicate HumanResource · highest value

Attendance_auto_decision_handler::preview() re-implements the payroll-lock, rolling-window and overlap rules instead of calling the service that owns them — because those checks are interleaved with the mutation inside transition_attendance_status() and cannot be invoked alone.

The handler documents this as deliberate, and the service's refusal correctly overrides the handler's verdict, so the system is safe. But preview and live can disagree about a record right up to the moment of the write.

Named fix: extract evaluate_attendance_transition($log_id, $status, $opts): array from those five pure read blocks (existence + scope, permissions, payroll lock, rolling window, overlap) and have transition_attendance_status() call it. Behaviour-preserving, and it gives preview true parity.

20.2 — The approve permission gate is inert HumanResource

AttendancesTrait.php:418-423$instance->status !== "pending" is false for every real approval, and the sibling identity check strict-compares a DB string to a session int, so it is always true. Preserved verbatim through the extraction, because fixing it would start denying approvals that work today, and a refactor whose criterion is "nothing changed" must not also change who may approve. The automation does not depend on this gate — publisher authority is its barrier. Raise as its own change with its own regression budget.

20.3 — access_type / allowed_members are dead inputs on this path HumanResource

Attendance_model::get_details() guards row-scoping with if (!$id && …), and transition_attendance_status() always passes an id. Both update_status() and bulk_update_attendance() pass scope options that have no filtering effect. The permission gate is the only barrier — and per 20.2 it is inert. This is precisely why the automation resolves its scope itself, up front, and why publisher authority is revalidated at run time.

20.4 — TeamAttendance_euapi::update_status() has not converged Deliberate

Own save, own metrics recompute, own lock check, no overlap check. Deliberate: it extends EndUserApiAuthController, and the trait's metrics path reaches an App_Controller helper — forcing the trait in would violate the API-controller rules. Its worst gap, the missing set_system_logs(), is fixed, tagged remarks: api:<status>. Full convergence needs the 20.1 extraction first, since a pure predicate is callable from an API controller in a way the mutating trait is not.

20.5 — CLI and HTTP cron locks do not intersect Runtime · deploy runbook

FlockLockProvider (CLI) and DbLockProvider (HTTP) use separate namespaces, so running both drivers can double-fire the dispatcher. Contained by the UNIQUE idempotency_key — which is why M1's duplicate-run test matters — but the operational rule is one driver only, and it belongs in the deploy runbook. This is runtime-level, not module-level.

20.6 — Analytics aggregates in PHP Fixed by M4.2

tabs/analytics.php sums the last 200 run rows in a foreach. Unbounded as history grows, and counting belongs in SQL. The named fix is Automation_runs_model::get_analytics_totals(), listed under What is not shipped.

20.7 — user_ids === array() emits AND 1=2 Not a bug — pin it

In both get_auto_decision_candidates() and its counter, deliberately: an explicitly empty configured scope means "nobody", not "everybody", and scope_user_ids() returns array() rather than null to say so. The failure mode this prevents is an automation scoped to a since-emptied department silently acting on the entire company. Pin it with a test so nobody "simplifies" it into an unscoped query.

20.8 — automations_reason_codes() has drifted and validates nothing Automations

Detail under Phantom APIs. There are two ways to close it and they are not equivalent. Make the list descriptive — regenerate it from the three real sources and keep the test as the pin. Or make it normative — have record_item() validate reason_code the way it already validates outcome. Normative is stricter and would have caught this, but it converts an unknown code from a harmless service passthrough into a write failure, so the list must be complete first. Descriptive in M4; consider normative in M5.

20.9 — The gateway's authority bypass rests on three flags, and a miss is a fatal HumanResource · undecided

Attendance_transition_gateway does not declare with_permission(), and __get forwards properties, not method calls — so a reachable call is a fatal Call to undefined method. Its docblock claimed the opposite ("returns FALSE here, always") until 2026-09-04; the claim was never true. Three separate options keep every trait call site unreachable: check_permissions => false, metrics_override => false, ot_permission => true.

The decision, owned by HumanResource: declaring the method removes the trip-wire, but the safe return value is not obvious. false denies at two call sites and skips at a third — both fine — yet at the overtime site it would silently queue every record's overtime as pending instead of failing loudly, which is a quiet pay-withholding change. Throwing is arguably more consistent with this class's own "refuse rather than approve unattributed" stance. Not decided.

20.10 — Two headless hosts of AttendancesTrait, shimmed differently HumanResource

Attendance_transition_gateway (cron) and checkfix/AttendanceMetricsCheck (web console) solve the same problem differently. The former aliases the trait constructor away, forwards __get to get_instance(), delegates var_biometrics_option() to erpat_biometrics_option(), and declares no with_permission(). The latter overrides the constructor plainly, forwards __get to its host controller, delegates var_biometrics_option() to that host (and throws under CLI), and declares with_permission() returning true.

Neither is wrong for its own caller, but a third host must extract the shim rather than copy it a third time — copy drift across hosts is exactly what the transition-service extraction was undertaken to end. Note also that approve_attendance() has four call sites, not one.


Decisions you might otherwise try to reverse

Each of these looks, at first glance, like something that should have been done the other way. Each was driven by a verified repository fact, so reversing one means re-checking that fact first.

One cron job, not a custom dispatcher

The original external draft specified a cron registration file, a distributed lock, a heartbeat table, a provider due-index and a catch-up policy. ERPat's Advanced Cron runtime already provides every one of them. The module registers a single JobContract class and nothing else. Both provider-level tables were dropped for the same reason: Tick already fans out per tenant with the database switched, so the job reads the current tenant's own table, and liveness is /cron/health plus cron_runs. Per-automation schedules reuse App\Cron\Core\CronExpression — it is ERPat's single parse authority, and a second parser would drift.

Publisher authority, not a service principal

The draft wanted a "virtual service principal". ERPat has no service-principal primitive, and inventing one is not a module's job. What is buildable today is publish-time and run-time revalidation of the publisher's own permissions, with the audit actor being the seeded System user. This has a property a service principal would not: authority fails closed — a publisher who is deleted, deactivated or stripped of automations_go_live moves the automation to needs_review and fails the run PUBLISHER_UNAUTHORIZED, rather than continuing to approve records on the authority of someone who has left.

????
Observe runs are deliberately NOT gated by that revalidation, and this is easy to mistake for an oversight. An Observe run changes nothing, so there is no authority to exercise — and stopping a read-only report because its author left removes exactly the visibility a reviewer needs at that moment. Related: never use user_has_permit() here. It ignores its $userid argument entirely and answers for the session user, which on a cron tick is nobody.

Plural, slug-keyed permission names

The plan first specified singular keys; the shipped keys are plural (automations, automations_create, …). The make:module default and the majority ERPat precedent both key permissions to the module slug (Todo→todo, Notes→note; Assets→asset is the outlier), and keeping the base equal to the slug removes a singular/plural split between with_module() and with_permission().

These ten keys are frozen. They shipped in 0.1.0 and are serialized into users.permissions and roles.permissions by name. Renaming one now is a data migration, not a rename. The same fact closed the open question about the module's name: "Automation Center" is settled because the slug, the module_automations setting and all ten keys are already out.

Administration placement, and its three core-side companions

The plan originally put the module in the Productivity group. It ships in Administration, between System Logs and Settings, with category "System" — because the Automation Center configures how ERPat acts on its own records. That is governance, not a productivity app, and it reads naturally beside the other two system surfaces.

The move required three core-side companions, and a future reader relocating this item needs all three:

Left_menu::_group_administration_items() — the member order.
Navigation_registry::_administration_item_names() — the Left Menu V2 registry's matching list.
application/config/left_menu.php — the core administration slice had to be split so settings keeps its own slice at position 999 and stays last.

The item name was unchanged by the move, so no saved custom menu was orphaned and no remap migration was needed. Three contract tests pin the placement across the module config and both core lists.

Approve-only, and the scaffolding that stayed

Phase 1 ships no Reject action. Attendance_auto_decision_handler::validate_config() refuses any other action with automation_error_action_not_supported. The reasoning: an Observe run can prove a rule selects the right records, but it cannot prove a rejection is correct — and a wrong rejection withholds earned pay invisibly until payday.

The forward-compatible scaffolding is deliberate and should not be tidied away: rejected_count and would_reject on the run tables, publish_permissions['reject'] in the sidecar, and the paused demo-seeder row.

The overtime policy destroys nothing

Worth restating here because it is the decision most likely to be misread as a data bug. The measured overtime hours are always stored. The policy records a decision about them in attendance_metrics.ot_status, and unapproved hours are withheld at read time. So skip (the default) decides nothing and queues the record for a person; exclude approves base hours and rejects the overtime; include approves both. No policy erases a number, and every choice is reversible from the Overtime tab.

Idempotency without a revision column

The draft's staleness guard assumed an attendance revision number. The attendance table has no revision, created_at or updated_at column, so the guard was reformulated: a transactional status re-read immediately before the write, plus UNIQUE(run_id, subject_type, subject_id) for per-run dedupe. Similarly, the draft's "pending attendance correction request" rule was dropped outright — that feature does not exist in ERPat — and replaced with "no pending schedule change request", which does.


Re-verifying this page

This page ages the moment M4 starts. Before trusting it against an unfamiliar checkout, re-run the three cheap checks that would catch most drift:

# 1. Is the phantom still phantom? Each of these should return the DECLARING
#    file and the docblock only — never a consumer.
grep -rn "publish_permissions" --include=*.php modules/
grep -rn "Automation_context"  --include=*.php modules/

# 2. Have the reason codes converged? Compare the declared vocabulary
#    against what the handler and the executor actually emit.
grep -n "automations_reason_codes" -A 25 modules/Automations/helpers/automations_helper.php
grep -oP "'[A-Z_]{4,}'" modules/HumanResource/automation/Attendance_auto_decision_handler.php | sort -u

# 3. Has M4 landed? Any of these existing means this page is out of date.
ls modules/Automations/config/notifications.php \
   modules/Automations/config/api_routes.php \
   modules/Automations/config/api_scope.php \
   modules/Automations/controllers/Automations_api.php 2>/dev/null

Then re-read modules/Automations/specs/automation-center-status.md §1 for the milestone table and §20 of the implementation plan for the debt register. Both are maintained as work lands; this page is a reading of them at a point in time.

????
If you close one of these gaps, close it in three places. The code, the plan's §20 entry (delete it — do not leave a fixed item claiming to be debt), and this page. A debt register that lists solved problems trains readers to ignore it, which costs more than the register was ever worth.

The one question still open

Three of the four open questions closed during implementation: the module name stays "Automation Center", the transition-service extraction was accepted and owned by HumanResource, and Phase 1 is approve-only. One remains: which tenant and which department host the observe-versus-human comparison pilot. Owner: Operations. Needed by M5, and it is the last thing between the module and a staged rollout.

Was this guide helpful?

Report a content problem