From a2d8895cd9e84697120193ce4587481978dd8f1f Mon Sep 17 00:00:00 2001 From: abhiram-vad Date: Mon, 3 Aug 2026 14:50:28 -0700 Subject: [PATCH 1/5] fix(maestro-case): harden Phase 0 eval contracts --- skills/uipath-maestro-case/SKILL.md | 2 +- .../references/phase-0-interview.md | 10 +- .../references/sdd-generation-rules.md | 5 +- .../_shared/check_credit_analyst_gate.py | 96 +++++++++++++ .../_shared/test_credit_analyst_gate.py | 43 ++++++ .../check_finalize_inventory.py | 78 +++++++++++ .../finalize_from_draft_loan.yaml | 15 +- .../test_finalize_inventory.py | 23 ++++ .../loan_origination/loan_origination.yaml | 2 +- .../check_procurement_sla_interrupts.py | 128 +++++++++++++++++- .../test_check_procurement_sla_interrupts.py | 39 ++++++ 11 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py create mode 100644 tests/tasks/uipath-maestro-case/_shared/test_credit_analyst_gate.py create mode 100644 tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/check_finalize_inventory.py create mode 100644 tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/test_finalize_inventory.py create mode 100644 tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/test_check_procurement_sla_interrupts.py diff --git a/skills/uipath-maestro-case/SKILL.md b/skills/uipath-maestro-case/SKILL.md index 4daad9f3cf..e81967f480 100644 --- a/skills/uipath-maestro-case/SKILL.md +++ b/skills/uipath-maestro-case/SKILL.md @@ -40,7 +40,7 @@ When `sdd.md` is absent, **Phase 0** designs the case by best assumption from th 10. **Cross-task refs:** plan as `"Stage Name"."Task Name".output_name`. Resolve both whole-value `<-` and in-expression `$xref` through the common output-reference-ID algorithm in [`plugins/variables/io-binding/impl-json.md`](references/plugins/variables/io-binding/impl-json.md#output-reference-id-authoritative): use the source output's `.id`; only a custom `=` output, which intentionally has no `.id`, resolves through its verified root companion's `.id`. Never use a reassigned output's `.var` as the source reference ID — it points at the target Case variable and can differ from the collision-safe source `.id`. Discover output names via `uip maestro case spec` (connector tasks) or `uip maestro case tasks describe` (non-connector tasks) — never fabricate. **Inside** a larger `=js:` expression (composite payload, condition, SLA), use the in-expression marker `vars.$xref('Stage','Task','output')` instead — resolved at Step 11.5. See [references/bindings-and-expressions.md](references/bindings-and-expressions.md) and [`plugins/variables/io-binding/impl-json.md`](references/plugins/variables/io-binding/impl-json.md). 11. **Build-review preference decides the Phase 2 → Phase 3 boundary — captured ONCE, up front, never asked mid-build.** Capture it at journey start: greenfield-with-interview folds it into the single confirmation's Build options (`Build it — straight through` / `Build it — pause at the build preview` — [phase-0-interview.md § Confirm](references/phase-0-interview.md#confirm--the-single-checkpoint)); greenfield-with-provided-SDD asks it once right after the roadmap; non-interactive runs and resumed runs with no recorded preference default to **straight-through** (no mid-build publish — Phase 6 stays the only publish point and stays gated). At the boundary, always run `validate --skeleton` (structural checks only) and print the counts summary — advisory, never halt on its errors. Then: **straight-through** → continue into Phase 3 with no prompt, the summary line doubling as the milestone narration; **pause-at-preview** → follow the publish-for-review contract in [`references/phased-execution.md`](references/phased-execution.md) (AskUserQuestion `Publish for review` / `Skip publish and continue` / `Abort`; on publish, print `DesignerUrl` as plain text BEFORE the follow-up prompt — never only inside the question body). Hard stops that are NEVER bypassed regardless of preference: Phase 4 retry exhaustion (`Retry with fix` / `Pause for manual edit` / `Abort`), Phase 5 entry (`Run debug session` / `Skip to Publish`), and Phase 6 entry (`Publish to Studio Web` / `Done`). Full contract in [`references/phased-execution.md`](references/phased-execution.md). 12. **Never run `uip maestro case debug` automatically.** Executes case for real — emails, messages, API calls. Explicit user consent only. -13. **All skill artifacts: Read + Write/Edit only.** Applies to `caseplan.json`, `sdd.md`, `sdd.draft.md`, `tasks.md`, `tasks/registry-resolved.json`, `tasks/trigger-spec-cache.json`, `tasks/spec-cache..json`, `bindings_v2.json`, `id-map.json`, `entry-points.json`, `build-issues.md`. No `python`, `node`, `jq`, `sed`, `awk`, or scripts that open/parse/modify/save these files. **Specifically forbidden** (common slip): `node -e "...fs.writeFileSync..."`, `node -e "...fs.readFileSync..."`, `node -e "..." > `, `jq '...' > `, `python -c "...open(...,'w')..."`, `sed -i`, `awk -i inplace`, or any shell redirection (`>`, `>>`, `| tee`) onto a skill artifact regardless of interpreter. **Writing a helper script under `/tmp` or anywhere else to assemble a skill artifact is also forbidden** — the build-assembler pattern (`/tmp/build-caseplan.js`, `/tmp/gen-tasks.py`, etc.) is the same Rule 13 violation as inline `node -e`, regardless of "mechanical copy" or "avoid Read+Write churn" framing. If `caseplan.json` exceeds ~30KB and a single Write feels too large, split into the Phase-2-skeleton-then-Phase-3-fill cadence (per [case-editing-operations.md § Per-section batch write contract](references/case-editing-operations.md#per-section-batch-write-contract--canonical)) — never via helper script. **The `node -e ... fs.*` ban is not scoped to the artifact list — it applies to ALL file reads in this skill, including resource cache reads from `~/.uip/case-resources/`. Use `cat ... | python3 -c "..."` or the `Read` tool for cache lookups.** Bash subprocesses OK ONLY for UUID v4 generation (`node -e "console.log(crypto.randomUUID())"` for `operate.json.projectId` and `entry-points.json` `uniqueId` — subprocess MUST NOT `require('fs')` or use redirection), CLI metadata fetches, validate, debug, and solution scaffold/upload. **Prefixed IDs (`Stage_`, `t`, `Rule_`, etc.) are picked inline by the agent — no subprocess.** See [references/case-editing-operations.md § Tool usage](references/case-editing-operations.md#tool-usage--mandatory). +13. **All skill artifacts: Read + Write/Edit only.** Applies to `caseplan.json`, `sdd.md`, `sdd.draft.md`, `tasks.md`, `tasks/registry-resolved.json`, `tasks/trigger-spec-cache.json`, `tasks/spec-cache..json`, `bindings_v2.json`, `id-map.json`, `entry-points.json`, `build-issues.md`. No `python`, `node`, `jq`, `sed`, `awk`, or scripts that open/parse/modify/save these files. **Shell filesystem commands may not create, replace, rename, or relocate an artifact** — `cp`, `mv`, `install`, and `rsync` are forbidden for these files, including copying or renaming `sdd.draft.md` to `sdd.md`. **Specifically forbidden** (common slip): `node -e "...fs.writeFileSync..."`, `node -e "...fs.readFileSync..."`, `node -e "..." > `, `jq '...' > `, `python -c "...open(...,'w')..."`, `sed -i`, `awk -i inplace`, or any shell redirection (`>`, `>>`, `| tee`) onto a skill artifact regardless of interpreter. **Writing a helper script under `/tmp` or anywhere else to assemble a skill artifact is also forbidden** — the build-assembler pattern (`/tmp/build-caseplan.js`, `/tmp/gen-tasks.py`, etc.) is the same Rule 13 violation as inline `node -e`, regardless of "mechanical copy" or "avoid Read+Write churn" framing. If `caseplan.json` exceeds ~30KB and a single Write feels too large, split into the Phase-2-skeleton-then-Phase-3-fill cadence (per [case-editing-operations.md § Per-section batch write contract](references/case-editing-operations.md#per-section-batch-write-contract--canonical)) — never via helper script. **The `node -e ... fs.*` ban is not scoped to the artifact list — it applies to ALL file reads in this skill, including resource cache reads from `~/.uip/case-resources/`. Use `cat ... | python3 -c "..."` or the `Read` tool for cache lookups.** Bash subprocesses OK ONLY for UUID v4 generation (`node -e "console.log(crypto.randomUUID())"` for `operate.json.projectId` and `entry-points.json` `uniqueId` — subprocess MUST NOT `require('fs')` or use redirection), CLI metadata fetches, validate, debug, and solution scaffold/upload. **Prefixed IDs (`Stage_`, `t`, `Rule_`, etc.) are picked inline by the agent — no subprocess.** See [references/case-editing-operations.md § Tool usage](references/case-editing-operations.md#tool-usage--mandatory). 14. **Resolved resources must be runnable, and sidecar parity is an unconditional Phase 3 exit check.** Before Phase 4, run Step 12 Checks 7, 9, and 11 even when publish, debug, and `uip solution resources refresh` are skipped. A task with a non-null `selected` entry in `tasks/registry-resolved.json` MUST NOT be emitted as a placeholder: it must remain present with `data.name` and `data.folderPath` bound to complete root bindings, its resource must project into `bindings_v2.json.resources[]`, and its binding pair's `resourceKey` must be self-consistent with its own `name`/`folderPath` defaults (Check 11) — never a copied tenant identity/UUID. `uip maestro case validate` success does not substitute for these checks. On a mismatch, repair the named task/binding or regenerate the sidecar once as applicable, then re-check; halt before Phase 4 if any of these checks still fails. Repeat Check 7 before every `resources refresh`. Always run `resources refresh` before `uip solution upload` or `uip maestro case debug` so Studio Web can resolve dependencies. 15. **Never auto-invoke `uipath-planner`.** If the user asks for planning across products, print a plain-text suggestion of the skill name; the user re-invokes it manually. No tool-call cross-skill handoff. 16. **Caseplan task `type` enum is closed — 9 values, schema-kebab.** Any task node written into `caseplan.json` MUST have `type` exactly one of: `process` | `agent` | `rpa` | `action` | `api-workflow` | `case-management` | `execute-connector-activity` | `wait-for-connector` | `wait-for-timer`. **Never** write the plugin folder name (`connector-activity`, `connector-trigger`) or the CLI `--type` flag value into the JSON node — those name the planning artifacts, not the schema. Never write `external-agent`, `external-workflow`, `document-extraction`, `flow-process`, `wait-for-event`, or any hallucinated value — there is no plugin to back them. `external-agent`, `external-workflow`, `document-extraction`, and `flow-process` are **not supported yet**. See [references/case-schema.md § Task type](references/case-schema.md) and the Plugin Index naming-asymmetry table below. diff --git a/skills/uipath-maestro-case/references/phase-0-interview.md b/skills/uipath-maestro-case/references/phase-0-interview.md index c0b7e0aae3..1166206cd5 100644 --- a/skills/uipath-maestro-case/references/phase-0-interview.md +++ b/skills/uipath-maestro-case/references/phase-0-interview.md @@ -106,9 +106,11 @@ Fill the complete SDD shape against [`sdd-template.md`](../assets/templates/sdd- **Structure rules while sketching:** §1.5 declare-vs-xref — mint a §1.5 row ONLY for `In`/`Out` args, trigger-payload Variables, and state read by a condition or ≥ 2 consumers; a single upstream output feeding one consumer is referenced directly (`<- "Stage"."Task".out` / `vars.$xref(...)`), never relayed. Required fields (case name, prefix, ≥1 trigger, ≥1 stage, ≥1 task per stage with type, ≥1 case exit) must all be settled — by user input or by playbook assumption. +**Conditional role / step gates must be inspectable.** When the source states a thresholded actor or step (for example, "Credit Analyst only over $5M; otherwise Underwriter"), model it as a guarded rule, task, recipient, or computed owner field AND preserve the business phrase close to the threshold in the draft/SDD text. A reviewer and a mechanical grep should be able to see both the actor name and threshold in one rule/task/rationale line, e.g. `Credit Analyst route when loanAmount > 5000000` or `Credit Analyst for loans >$5M; Underwriter otherwise`. Do not leave the gate only in a persona table or detached prose. + **Other-path sweep — mandatory before confirmation.** Do not design only the primary flow and wait for the user to ask about alternatives later. Check the source for: rework / needs-info loops; rejection, withdrawal, and cancellation; SLA escalation; external-system failure; manual override or worker-selected side work; optional side work; and terminal outcomes that differ from successful completion. For each scenario, choose the correct model: interrupting secondary stage, terminal case-exit, non-completing case-exit, task-level branch, `adhoc` task, SLA notification only, or "not modeled" when the source explicitly rules it out. If the source names or strongly implies a scenario, model it by best assumption and disclose it in **Other Paths Considered**. If the source has no signal at all, spend the one clarifying call on a single bounded question before confirmation: "I don't see any other paths beyond the primary flow. Should I add standard paths for rework, cancellation/withdrawal, SLA escalation, or keep only the primary flow?" -**Buildability musts** — settle all ten by assumption and surface each in the confirmation; they are where designs silently become unbuildable: (1) other-path trigger source (gate decision → `selected-stage-completed/-exited` + IF; person → `user-selected-stage` only with an upstream `wait-for-user` exit; external/global event → one `wait-for-connector` entry on the secondary stage; SLA at-risk/breach that requires case work → one `sla-status-change` entry whose target and SLA title — plus an at-risk escalation title for an at-risk row only — are declared in the SDD, while warning-only escalation stays a notification; interrupting flags on stage + entry rows; terminal `exit-only` vs `return-to-origin`; never duplicate global-event exits/tasks across primary stages); (2) every decision outcome routes somewhere — no dead-end status values, and an outcome that targets a lane keys that lane's entry; (3) every configure/decide task's output lands in a variable or direct reference; (4) every send/connector/agent's required inputs map to variables/literals/upstream outputs as far as knowable without schemas — the rest resolves at build; (5) conditional roles/steps become guarded rules + personas, not prose; (6) a critical-path connector failure gets a modeled other path when the user described failure handling — otherwise note it as an architect advisory; (7) manual-surface classification per the playbook: human-performed required work is `action`, optional user-launched work is `adhoc`; (8) intended resource names concrete, identities per the light pass; (9) every stage/task/SLA has durable rationale in the model, including why an ordered run is sequential, independent work is parallel, or parallel-after-predecessor siblings share one task set; (10) every non-start entry rule has a concrete producer/reference. +**Buildability musts** — settle all ten by assumption and surface each in the confirmation; they are where designs silently become unbuildable: (1) other-path trigger source (gate decision → `selected-stage-completed/-exited` + IF; person → `user-selected-stage` only with an upstream `wait-for-user` exit; external/global event → one `wait-for-connector` entry on the secondary stage; SLA at-risk/breach that requires case work → one `sla-status-change` entry whose target and SLA title — plus an at-risk escalation title for an at-risk row only — are declared in the SDD, while warning-only escalation stays a notification; interrupting flags on stage + entry rows; terminal `exit-only` vs `return-to-origin`; never duplicate global-event exits/tasks across primary stages); (2) every decision outcome routes somewhere — no dead-end status values, and an outcome that targets a lane keys that lane's entry; (3) every configure/decide task's output lands in a variable or direct reference; (4) every send/connector/agent's required inputs map to variables/literals/upstream outputs as far as knowable without schemas — the rest resolves at build; (5) conditional roles/steps become guarded rules + personas, not prose, with the actor and threshold visible together in the draft/SDD; (6) a critical-path connector failure gets a modeled other path when the user described failure handling — otherwise note it as an architect advisory; (7) manual-surface classification per the playbook: human-performed required work is `action`, optional user-launched work is `adhoc`; (8) intended resource names concrete, identities per the light pass; (9) every stage/task/SLA has durable rationale in the model, including why an ordered run is sequential, independent work is parallel, or parallel-after-predecessor siblings share one task set; (10) every non-start entry rule has a concrete producer/reference. **The one clarifying call (rare).** Ask before the confirmation ONLY when: (a) no case is inferable at all (empty or contentless request), (b) the user's own inputs contradict each other on a shape-changing field, (c) the user asked to be asked, or (d) the mandatory other-path sweep found no source signal at all. Batch everything into ONE AskUserQuestion call (≤ 4 questions). An unclear answer → take the best assumption, disclose it, move on — never re-press. Everything else: assume and inform. @@ -188,8 +190,10 @@ Compact `tasks/tasks.md` contract for this no-build path: - Use machine-scannable task headings in the plan: `## T{N}: task "{Task Name}"`. Do not hide task T-entries under dotted subheadings such as `### T12.1`; nested prose is allowed under the H2, but the task entry itself uses a plain integer T-number and quotes the task name. - Stage entries include `stage-kind`, `entry-rule`, `exit-rule`, `interrupting`, `required`, `sla`, and `rationale`. - Task entries include `stage`, `type`, `activation-mode`, `entry-rule`, `lane`, `required`, `run-only-once`, `resource-intent`, `identity: resolve at build`, and `rationale`. +- Preserve each task's confirmed SDD activation semantics exactly. A singleton task that starts with its stage remains `activation-mode: parallel` + `entry-rule: current-stage-entered`; a single-task stage or list position does not make it sequential. Use `sequential` + `runs-sequentially` only when the source explicitly requires an ordered run or dependency. - Sequential runs use consecutive single-task lane numbers; every task in the run has `activation-mode: sequential` and `entry-rule: runs-sequentially`. -- Global event/exception entries name exactly one interrupting secondary stage and the rule type (`wait-for-connector` or `sla-status-change`); do not duplicate those events across every primary stage. A `sla-status-change` entry names target + SLA title, plus an at-risk escalation title only for an at-risk row (a breach names the SLA alone) — all declared in the SDD. +- When the prompt says every primary phase/stage has an SLA target, every named primary stage renders its own `#### Stage SLA` block with a concrete `**SLA Title:**` (prefer ` SLA`) and concrete at-risk/breach display names. Every `sla-status-change` reference uses those exact titles. +- Global event/exception entries name exactly one interrupting secondary stage and the rule type (`wait-for-connector` or `sla-status-change`); do not duplicate those events across every primary stage. A `sla-status-change` entry names target + SLA title, plus an at-risk escalation title only for an at-risk row (a breach names the SLA alone) — all declared in the SDD and repeated verbatim in `tasks/tasks.md`. - Do not add `taskTypeId`, `activityTypeId`, `connectionId`, resolved schemas, `inputs`, `outputs`, `registry-resolved.json`, or `recipients-resolved.json`. - End the response with suggested next steps: review the SDD/plan, then run a later build to resolve tenant resources and create `caseplan.json`. @@ -211,7 +215,7 @@ Generation: Read [`assets/templates/sdd-viewer.html`](../assets/templates/sdd-vi If the user explicitly asks to finalize the existing draft, choose `Use the draft — finalize and continue` by assumption and do not ask a redundant resumption question. If AskUserQuestion is unavailable, make the same assumption unless the user asked to discard or abort. Finalization stays inside this skill: render the final `sdd.md` from the Case Management template and run the template conformance gate; never route `sdd.draft.md` finalization to `uipath-planner`. -**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure only: every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. Then write `sdd.md` and stop. +**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure only: inventory the draft's stage and task headings in memory, then render one complete output block for each; never use `cp`, `mv`, `install`, `rsync`, or another shell copy/rename operation to turn the draft into the final artifact. Every existing stage gets `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`. Every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Use concise default detail tables when the draft has only task summaries, but preserve exact stage and task display names (including punctuation), task types, variables, conditions, connector placeholders, and domain rules; structural normalization never renames business elements. A thresholded actor in draft prose/personas must also become executable inside an existing task — use a guarded owner/recipient/assignment expression that names the threshold and actor on the same line (for example, `=js:vars.loanAmount > 5000000 ? "Role:CreditAnalyst" : "Role:Underwriter"`); persona prose alone is not final, and this normalization must not add or rename a task. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. For a large draft that needs batched writes, first Write the complete ordered document skeleton — Sections 1–4 and every primary/secondary stage heading in source order inside Section 2 — then Edit each stage/task block in place. Never append a deferred or omitted stage after `## Section 3`; insert it at its existing Section 2 heading before continuing. Before writing, confirm the output has the same ordered stage/task inventory and that every stage/task block carries its required literal markers: stage `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`; task `**Activation Mode:**`, `**Design Rationale:**`, `**Task envelope**`, and the matching type-specific detail heading. Section 2 is incomplete until every inventoried stage and task appears before `## Section 3`. Then write `sdd.md` with Write/Edit and stop. ## What to say while working diff --git a/skills/uipath-maestro-case/references/sdd-generation-rules.md b/skills/uipath-maestro-case/references/sdd-generation-rules.md index 78038d1573..9783cbfbb4 100644 --- a/skills/uipath-maestro-case/references/sdd-generation-rules.md +++ b/skills/uipath-maestro-case/references/sdd-generation-rules.md @@ -90,8 +90,9 @@ How to reason with these: - **`required-*` vs `selected-*`.** `required-tasks-completed` / `required-stages-completed` = "all items flagged required are done" (the `isRequired` flow). `selected-tasks-completed` / `selected-stage-completed` / `selected-stage-exited` = "these *specific named* items." Pairing rule (Key Rule 4): `Marks Complete: Yes` pairs only with `required-*`; `selected-*` is for `No` (routing / early exit / alternate disposition). A `Yes` + `selected-*` pair is a schema error. - **Secondary stage** uses **stage-entry + stage-exit rules only, never edges** (true of every stage — edges retired). Its entry rules are always interrupting (`isInterrupting: true`). Returning exits use the canonical completion shape `return-to-origin` + `Marks Stage Complete: Yes` + `required-tasks-completed` to rejoin the flow they left; terminal exits use `exit-only` + `Marks Stage Complete: Yes` plus a root case-exit row. **For a decision/signal-routed lane, the routing lives on the *origin* stage:** a gated diverting exit (`Marks Stage Complete: No`, `IF` on the decision/signal, `exitToStageId` → the lane), with the origin's completion exit gated by the inverse `IF` so the two paths are mutually exclusive. **For a global connector/SLA event, routing lives only on the destination secondary stage:** no per-origin exit rule is needed. See [§ Logical integrity step 5](#logical-integrity--stage-graph). - **Task-entry mode is exclusive.** Use `current-stage-entered` only for stage-started/default tasks. Event-triggered tasks carry the explicit event rule (`wait-for-connector` with connector configuration), adhoc tasks carry only `adhoc`, and sequential tasks carry only `runs-sequentially`; do not add `current-stage-entered` to those tasks just because they are first in a stage. For sequential chains, the first task's `runs-sequentially` means current-stage-entered, and later tasks use it as the preceding-task-completed trigger. `wait-for-connector` makes a gate pause for an inbound connector callback — its `conditionExpression` gates on **case state** only (no `event` payload; in-rule extract-then-gate is unsupported at runtime — gate a downstream condition instead); `adhoc` lets a *task* fire manually from the case app (task-entry only — never a stage-entry rule). A `start-task` SLA response is event-triggered: the task carries `sla-status-change` as its only entry rule, never alongside `current-stage-entered`. +- **Thresholded role / work gates are executable, not descriptive.** A source rule like "Credit Analyst only over $5M; otherwise Underwriter" must produce a rule, task-entry condition, recipient expression, or computed owner field that references both the threshold and the gated actor/work. Preserve the reviewer-facing phrase close to the expression in the SDD, for example `Credit Analyst route when loanAmount > 5000000`. A persona row alone, or a generic note that does not tie `Credit Analyst` to `>$5M`, is not a modeled gate. -**Frontend task-mode mapping.** The UI's `sequential`, `event-triggered`, and `manually-triggered` choices are not interchangeable: sequential means the task-only `runs-sequentially` rule; event-triggered means an explicit event/condition rule (use `wait-for-connector` for an external connector callback); manually-triggered means an `adhoc`-only task with `isRequired: false`. `adhoc` decides how the task starts; it does not decide the task type. A manually triggered task may still be `action`, `agent`, `api-workflow`, `process`, etc. Do not infer one mode from `data.tasks` lanes, and do not add a second entry rule that changes the selected mode. +**Frontend task-mode mapping.** The UI's `sequential`, `event-triggered`, and `manually-triggered` choices are not interchangeable: sequential means the task-only `runs-sequentially` rule; event-triggered means an explicit event/condition rule (use `wait-for-connector` for an external connector callback); manually-triggered means an `adhoc`-only task with `isRequired: false`. `adhoc` decides how the task starts; it does not decide the task type. A manually triggered task may still be `action`, `agent`, `api-workflow`, `process`, etc. Do not infer one mode from `data.tasks` lanes, and do not add a second entry rule that changes the selected mode. Downstream plans preserve the confirmed SDD mode and rule exactly: a singleton `parallel` + `current-stage-entered` task stays that way, while `sequential` + `runs-sequentially` requires an explicit order or dependency in the source. - **`user-selected-stage`** (stage entry) starts a stage on demand by a user rather than by flow. The CLI validator requires it to pair with a `wait-for-user` stage exit elsewhere: a `wait-for-user` exit with no `user-selected-stage` entry — or a `user-selected-stage` entry with no `wait-for-user` exit — fails `validate`. Exact cell formats live in [§ Stage content rules](#stage-content-rules) and [§ Task content rules](#task-content-rules) — this table is the conceptual map of *which rule belongs where*. @@ -443,6 +444,8 @@ The trailing `` `{stage_id}` `` (e.g., `` `stage-intake` ``) MUST appear so read | Interrupting | secondary stages only | `Yes` — secondary stages are interrupting lanes. `No` only on an `sla-status-change` parallel-oversight row (§ mental model carve-out). Otherwise, if the work should not interrupt, model it as a regular stage/parallel path or an `adhoc` task. | | Stage SLA | yes when stage has SLA | Default duration + `time-based` or `condition-based` + `SLA Title` (non-empty, stage-unique, no `:`), plus conditional-rule and escalation tables | +When the source says every primary phase/stage has an SLA target, every named primary stage MUST render its own `#### Stage SLA` block. Use deterministic titles when the user did not supply names: `**SLA Title:** SLA`, at-risk display name ` SLA at risk`, and breach display name ` SLA breached`. Any `sla-status-change("",...)` row for that stage must use those exact strings; a title declared on another stage or only in prose does not resolve. + ### Stage Entry Conditions table ≥ 1 row required. diff --git a/tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py b/tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py new file mode 100644 index 0000000000..00a28f5338 --- /dev/null +++ b/tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Verify that the Credit Analyst is assigned only on the high side of $5M.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +THRESHOLD = r"(?:\$?\s*5\s*(?:m(?:illion)?|million)\b|5,?000,?000\b)" +HIGH_SIDE = rf"(?:>(?!=)\s*{THRESHOLD}|(?:over|above|greater\s+than|more\s+than|in\s+excess\s+of)\s*{THRESHOLD})" +LOW_SIDE = re.compile( + rf"(?:<=|<)\s*{THRESHOLD}|\b(?:at\s+or\s+below|below|under|up\s+to|no\s+more\s+than|not\s+more\s+than)\b", + re.IGNORECASE, +) +OTHER_ROLE = re.compile(r"\bunderwriter\b", re.IGNORECASE) +OTHER_ROLE_CONTRAST = re.compile( + r"\b(?:instead\s+of|rather\s+than|not)\s+(?:the\s+)?underwriter\b", + re.IGNORECASE, +) +NEGATION = re.compile( + r"\b(?:do\s+not|must\s+not|should\s+not|cannot|can't|never|not|ineligible)\b", + re.IGNORECASE, +) +POST_ACTOR_NEGATION = re.compile( + r"^\W*(?:role\s+)?(?:" + r"(?:must|should|can|is|are|be)\s+not\b|cannot\b|can't\b|never\b|ineligible\b|" + r"not\s+(?:be\s+)?(?:assigned|routed|required|eligible|permitted)\b)", + re.IGNORECASE, +) +EXECUTION_SIGNAL = re.compile( + r"(?:=js:|vars\.|\b\w*(?:owner|recipient)\b|\b(?:assign|assignment|route|condition|guard|when|if|task|require)\w*\b)", + re.IGNORECASE, +) + + +def has_credit_analyst_gate(text: str) -> bool: + """Accept either phrase order while requiring a high-side comparator.""" + patterns = ( + ( + re.compile( + rf"credit\s+analyst(?P.{{0,160}}?){HIGH_SIDE}", + re.IGNORECASE, + ), + False, + ), + ( + re.compile( + rf"{HIGH_SIDE}(?P.{{0,160}}?)credit\s+analyst", + re.IGNORECASE, + ), + True, + ), + ) + for line in text.splitlines(): + for clause in re.split(r";|(?<=[.!])\s+", line): + for pattern, reject_other_role_prefix in patterns: + for match in pattern.finditer(clause): + between = match.group("between") + if ( + LOW_SIDE.search(between) + or ( + OTHER_ROLE.search(between) + and not OTHER_ROLE_CONTRAST.search(between) + ) + or NEGATION.search(OTHER_ROLE_CONTRAST.sub("", match.group(0))) + or NEGATION.search(clause[: match.start()]) + or POST_ACTOR_NEGATION.search(clause[match.end() :]) + or not EXECUTION_SIGNAL.search(clause) + ): + continue + if reject_other_role_prefix and OTHER_ROLE.search( + clause[: match.start()] + ) and not LOW_SIDE.search(clause[: match.start()]): + continue + return True + return False + + +def main() -> None: + paths = [Path(value) for value in sys.argv[1:]] + existing = [path for path in paths if path.is_file()] + if not existing: + sys.exit("FAIL: no SDD artifact was found") + text = "\n".join(path.read_text(encoding="utf-8") for path in existing) + if not has_credit_analyst_gate(text): + sys.exit( + "FAIL: no executable high-side Credit Analyst gate ties the role " + "to loans over $5M" + ) + print("OK: Credit Analyst is gated to loans over $5M") + + +if __name__ == "__main__": + main() diff --git a/tests/tasks/uipath-maestro-case/_shared/test_credit_analyst_gate.py b/tests/tasks/uipath-maestro-case/_shared/test_credit_analyst_gate.py new file mode 100644 index 0000000000..9893f4de97 --- /dev/null +++ b/tests/tasks/uipath-maestro-case/_shared/test_credit_analyst_gate.py @@ -0,0 +1,43 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from check_credit_analyst_gate import has_credit_analyst_gate + + +@pytest.mark.parametrize( + "text", + ( + "Assign Credit Analyst for loans >$5M; Underwriter otherwise", + 'underwritingOwner = loanAmount > 5000000 ? "Credit Analyst" : "Underwriter"', + "Above $5 million, route the review to a Credit Analyst", + "Loans in excess of $5M require Credit Analyst review", + "Underwriter handles loans at or below $5M, while above $5M assign Credit Analyst", + "Above $5M assign Credit Analyst, not Underwriter", + "Assign Credit Analyst instead of Underwriter for loans over $5M", + ), +) +def test_high_side_gate_is_accepted_in_either_phrase_order(text): + assert has_credit_analyst_gate(text) + + +@pytest.mark.parametrize( + "text", + ( + "Credit Analyst handles loans at or below $5M; Underwriter above $5M", + 'loanAmount <= 5000000 ? "Credit Analyst" : "Underwriter"', + "Credit Analyst persona; Underwriter handles loans >$5M", + "Loans >$5M go to Underwriter; loans <=$5M go to Credit Analyst", + "Underwriter handles loans above $5M; Credit Analyst handles the rest", + "| Credit Analyst | Underwriting (loans >$5M only) | Reviews credit analysis |", + "Credit Analyst must not handle loans over $5M", + "Do not assign loans >$5M to Credit Analyst", + "Above $5M, Credit Analyst must not be assigned", + "Credit Analyst reviews every loan", + ), +) +def test_inverted_or_descriptive_gate_is_rejected(text): + assert not has_credit_analyst_gate(text) diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/check_finalize_inventory.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/check_finalize_inventory.py new file mode 100644 index 0000000000..94f749658d --- /dev/null +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/check_finalize_inventory.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Ensure finalization preserves the draft's ordered stage/task inventory.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +STAGE_HEADING = re.compile( + r"(?im)^###\s+(?:Stage\s+\d+|Secondary\s+Stage):\s*(.+?)\s*$" +) +TASK_HEADING = re.compile(r"(?im)^#####\s+Task\s+[^:\n]+:\s*(.+?)\s*$") + + +def clean_name(value: str) -> str: + return re.sub(r"\s+\(`[^`]+`\)\s*$", "", value).strip() + + +def inventory(text: str, pattern: re.Pattern[str]) -> list[str]: + return [clean_name(match.group(1)) for match in pattern.finditer(text)] + + +def stage_task_inventory(text: str) -> list[tuple[str, str]]: + events = [ + *((match.start(), "stage", clean_name(match.group(1))) for match in STAGE_HEADING.finditer(text)), + *((match.start(), "task", clean_name(match.group(1))) for match in TASK_HEADING.finditer(text)), + ] + current_stage = None + result = [] + for _, kind, name in sorted(events): + if kind == "stage": + current_stage = name + elif current_stage is None: + sys.exit(f"FAIL: task {name!r} appears before any stage heading") + else: + result.append((current_stage, name)) + return result + + +def require_same_order(kind: str, expected: list[object], actual: list[object]) -> None: + if expected == actual: + return + mismatch = next( + ( + index + for index, pair in enumerate(zip(expected, actual)) + if pair[0] != pair[1] + ), + min(len(expected), len(actual)), + ) + expected_item = expected[mismatch] if mismatch < len(expected) else "" + actual_item = actual[mismatch] if mismatch < len(actual) else "" + sys.exit( + f"FAIL: finalized {kind} inventory differs at position {mismatch + 1}: " + f"expected {expected_item!r}, got {actual_item!r} " + f"(draft={len(expected)}, final={len(actual)})" + ) + + +def main() -> None: + draft = Path("sdd.draft.md").read_text(encoding="utf-8") + final = Path("sdd.md").read_text(encoding="utf-8") + draft_stages = inventory(draft, STAGE_HEADING) + draft_stage_tasks = stage_task_inventory(draft) + if not draft_stages or not draft_stage_tasks: + sys.exit("FAIL: draft contains no stage/task inventory") + require_same_order("stage", draft_stages, inventory(final, STAGE_HEADING)) + require_same_order("stage/task", draft_stage_tasks, stage_task_inventory(final)) + print( + f"OK: finalized SDD preserves {len(draft_stages)} ordered stages and " + f"{len(draft_stage_tasks)} ordered stage/task assignments" + ) + + +if __name__ == "__main__": + main() diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/finalize_from_draft_loan.yaml b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/finalize_from_draft_loan.yaml index d99ef923cd..78805bd582 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/finalize_from_draft_loan.yaml +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/finalize_from_draft_loan.yaml @@ -3,7 +3,7 @@ description: > Phase 0 FINALIZATION ONLY — a LoanOrigination interview draft (sdd.draft.md) is staged into the sandbox; the skill resumes Phase 0, runs the Finalization checks (normalizing rule syntax / gate legality / interrupting flags as - needed), and renames it to an approved sdd.md. Decoupled from the interview + needed), and renders an approved sdd.md. Decoupled from the interview (covered by loan_origination) so the heavy finalization step runs in one bounded turn. Grades mechanical validity of the produced sdd.md (sdd_check) plus domain coherence. Loan-domain counterpart to finalize_from_draft. @@ -60,12 +60,19 @@ success_criteria: - type: run_command description: "sdd.md preserves the conditional >$5M Credit-Analyst gate" - command: "grep -Eiq 'credit analyst.{0,80}5' sdd.md" + command: "python3 $SKILLS_REPO_PATH/tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py sdd.md" timeout: 10 expected_exit_code: 0 weight: 1.0 pass_threshold: 1.0 + - type: command_not_executed + description: "Rule 13: finalization renders sdd.md with Write/Edit instead of copying or renaming the draft" + tool_name: "Bash" + command_pattern: '(cp|mv|install|rsync)\b[^\n]*(sdd\.draft\.md|sdd\.md)' + weight: 1.0 + pass_threshold: 1.0 + - type: run_command description: "sdd.md preserves all 4 exception lanes" command: "grep -Eiq 'customer comms' sdd.md && grep -Eiq 'escalation' sdd.md && grep -Eiq 'withdrawn' sdd.md && grep -Eiq 'rejected' sdd.md" @@ -75,8 +82,8 @@ success_criteria: pass_threshold: 1.0 - type: run_command - description: "finalized sdd.md preserves all 6 primary stages (none dropped at finalization)" - command: "[ \"$(grep -cE '^#+ +Stage [0-9]' sdd.md)\" -ge 6 ]" + description: "finalized sdd.md preserves the draft's complete ordered stage/task inventory" + command: "python3 $SKILLS_REPO_PATH/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/check_finalize_inventory.py" timeout: 10 expected_exit_code: 0 weight: 1.0 diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/test_finalize_inventory.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/test_finalize_inventory.py new file mode 100644 index 0000000000..4ba9af2682 --- /dev/null +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_loan/test_finalize_inventory.py @@ -0,0 +1,23 @@ +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from check_finalize_inventory import stage_task_inventory + + +def test_task_moved_to_another_stage_changes_inventory(): + draft = """ +### Stage 1: Intake +##### Task 1.1: Review Application +### Stage 2: Decision +##### Task 2.1: Record Decision +""" + moved = """ +### Stage 1: Intake +### Stage 2: Decision +##### Task 2.1: Review Application +##### Task 2.2: Record Decision +""" + + assert stage_task_inventory(draft) != stage_task_inventory(moved) diff --git a/tests/tasks/uipath-maestro-case/phase_0_to_case/loan_origination/loan_origination.yaml b/tests/tasks/uipath-maestro-case/phase_0_to_case/loan_origination/loan_origination.yaml index 96d75b7078..17ed112d43 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_to_case/loan_origination/loan_origination.yaml +++ b/tests/tasks/uipath-maestro-case/phase_0_to_case/loan_origination/loan_origination.yaml @@ -60,7 +60,7 @@ success_criteria: - type: run_command description: "design models the conditional >$5M Credit-Analyst gate" - command: "cat sdd.draft.md sdd.md 2>/dev/null | grep -Eiq 'credit analyst.{0,80}5'" + command: "python3 $SKILLS_REPO_PATH/tests/tasks/uipath-maestro-case/_shared/check_credit_analyst_gate.py sdd.draft.md sdd.md" timeout: 10 expected_exit_code: 0 weight: 1.0 diff --git a/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/check_procurement_sla_interrupts.py b/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/check_procurement_sla_interrupts.py index 8f0afa96c0..66fb59396f 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/check_procurement_sla_interrupts.py +++ b/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/check_procurement_sla_interrupts.py @@ -26,21 +26,124 @@ def has_near(text: str, left: str, right: str, distance: int = 500) -> bool: ) is not None -def task_section(plan: str, task_name: str) -> str: +def task_section(plan: str, task_name: str, stage_name: str | None = None) -> str: heading = ( rf"^#{{2,3}}\s+T\d+(?:\.\d+)?\s*(?:[:—-])\s*" rf"(?:Task:\s*)?(?:task\s+)?(?:\"{re.escape(task_name)}\"|{re.escape(task_name)}\b)[^\n]*\n" ) next_heading = rf"^#{{2,3}}\s+T\d+(?:\.\d+)?\s*(?:[:—-])" - match = re.search( + matches = list(re.finditer( rf"(?ims){heading}.*?(?={next_heading}|\Z)", plan, - ) + )) + if stage_name is not None: + matches = [ + match + for match in matches + if re.search( + rf'(?im)^-\s*stage:\s*["`]?{re.escape(stage_name)}["`]?\s*$', + match.group(0), + ) + ] + if not matches: + location = f" in stage {stage_name!r}" if stage_name else "" + fail(f"missing tasks.md T-entry for {task_name!r}{location}") + if len(matches) > 1: + location = f" in stage {stage_name!r}" if stage_name else "" + fail(f"ambiguous tasks.md T-entry for {task_name!r}{location}") + return matches[0].group(0) + + +def rule_type(value: str, task_name: str, field: str) -> str: + match = re.search(r"[a-z][a-z0-9-]*", value.casefold().replace("`", "")) if not match: - fail(f"missing tasks.md T-entry for {task_name!r}") + fail(f"missing {field} rule type for task {task_name!r}") return match.group(0) +def sdd_task_activation(sdd: str) -> dict[tuple[str, str], tuple[str, str]]: + """Return each SDD task's declared activation mode and entry-rule type.""" + headings = list( + re.finditer( + r"(?im)^#####\s+Task\s+[^:\n]+:\s*(.+?)\s*$", + sdd, + ) + ) + stage_headings = list(re.finditer(STAGE_HEADING, sdd)) + contracts: dict[tuple[str, str], tuple[str, str]] = {} + for index, heading in enumerate(headings): + end = headings[index + 1].start() if index + 1 < len(headings) else len(sdd) + section = sdd[heading.start() : end] + task_name = re.sub(r"\s+\(`[^`]+`\)\s*$", "", heading.group(1)).strip() + stage_heading = next( + (candidate for candidate in reversed(stage_headings) if candidate.start() < heading.start()), + None, + ) + if stage_heading is None: + fail(f"task {task_name!r} appears before any SDD stage heading") + stage_name = re.sub( + r"\s*\([^)]*\)\s*$", "", stage_heading.group(1) + ).strip() + + activation = re.search( + r"(?im)^\*\*Activation Mode:\*\*\s*([^\n]+)", + section, + ) + if not activation: + fail(f"missing SDD Activation Mode for task {task_name!r}") + + entry_block = re.search( + r"(?ims)^\*\*Entry Condition:\*\*\s*(.*?)(?=^\*\*Task envelope|^######|\Z)", + section, + ) + if not entry_block: + fail(f"missing SDD Entry Condition for task {task_name!r}") + entry_rule = None + for line in entry_block.group(1).splitlines(): + if not line.strip().startswith("|"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if not cells or cells[0].casefold() == "when" or re.fullmatch(r"[-:\s]+", cells[0]): + continue + entry_rule = rule_type(cells[0], task_name, "SDD entry") + break + if entry_rule is None: + fail(f"missing SDD entry-rule row for task {task_name!r}") + + key = (stage_name, task_name) + if key in contracts: + fail(f"duplicate SDD task {task_name!r} in stage {stage_name!r}") + contracts[key] = ( + rule_type(activation.group(1), task_name, "SDD activation"), + entry_rule, + ) + + if not contracts: + fail("SDD declares no task detail blocks") + return contracts + + +def check_plan_preserves_task_activation(sdd: str, plan: str) -> None: + """The no-build handoff must not reinterpret confirmed task semantics.""" + for (stage_name, task_name), (sdd_activation, sdd_entry_rule) in sdd_task_activation(sdd).items(): + section = task_section(plan, task_name, stage_name) + activation = re.search(r"(?im)^-\s*activation-mode:\s*([^\n]+)", section) + entry_rule = re.search(r"(?im)^-\s*entry-rule:\s*([^\n]+)", section) + if not activation: + fail(f"missing tasks.md activation-mode for task {task_name!r}") + if not entry_rule: + fail(f"missing tasks.md entry-rule for task {task_name!r}") + + plan_activation = rule_type(activation.group(1), task_name, "plan activation") + plan_entry_rule = rule_type(entry_rule.group(1), task_name, "plan entry") + if plan_activation != sdd_activation or plan_entry_rule != sdd_entry_rule: + fail( + f"tasks.md changes {task_name!r} activation from " + f"{sdd_activation}/{sdd_entry_rule} to " + f"{plan_activation}/{plan_entry_rule}" + ) + + def stage_section(sdd: str, stage_name: str) -> str: heading = rf"^#{{2,4}}\s+(?:Secondary\s+Stage:\s*)?{re.escape(stage_name)}\b[^\n]*\n" next_stage = r"^#{2,4}\s+(?:Stage\s+\d+|Secondary\s+Stage:)" @@ -148,6 +251,18 @@ def declared_sla_titles(sdd: str) -> set[str]: } +def check_canonical_stage_sla(section: str, stage: str) -> None: + if re.search(r"(?im)^####\s+Stage SLA\s*$", section) is None: + fail(f"primary phase {stage!r} has no canonical '#### Stage SLA' block") + titles = re.findall(r"(?im)^\*\*SLA Title:\*\*\s*(.+)$", section) + expected = f"{stage} SLA" + if [title.strip().casefold() for title in titles] != [expected.casefold()]: + fail( + f"primary phase {stage!r} must declare exactly " + f"'**SLA Title:** {expected}'; got {titles or 'nothing'}" + ) + + def check_sla_reference_closure(sdd: str) -> None: """Every sla-status-change entry must resolve to a declared SLA + escalation. @@ -211,6 +326,7 @@ def check_sla_reference_closure(sdd: str) -> None: ) if section is None: fail(f"missing SDD stage section for primary phase {stage!r}") + check_canonical_stage_sla(section, stage) if not declared_sla_titles(section): fail( f"primary phase {stage!r} declares no stage SLA (prompt: every primary " @@ -290,13 +406,15 @@ def main() -> None: f"expected {expected_lanes!r}" ) + check_plan_preserves_task_activation(sdd, plan) + if sdd.lower().count("rationale") < 4: fail("SDD does not preserve enough design rationale") if plan.lower().count("rationale") < 4: fail("tasks.md does not carry the SDD rationale into planning") print( - "OK: global interrupts, resolvable SLA references, sequential activation, " + "OK: global interrupts, resolvable SLA references, task activation, " "and rationale are preserved" ) diff --git a/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/test_check_procurement_sla_interrupts.py b/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/test_check_procurement_sla_interrupts.py new file mode 100644 index 0000000000..ef7fede933 --- /dev/null +++ b/tests/tasks/uipath-maestro-case/phase_0_to_case/procurement_sla_interrupts/test_check_procurement_sla_interrupts.py @@ -0,0 +1,39 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from check_procurement_sla_interrupts import check_canonical_stage_sla, task_section + + +def test_duplicate_task_names_are_resolved_by_stage(): + plan = """ +## T01: task "Review" + +- stage: Intake +- activation-mode: parallel + +## T02: task "Review" + +- stage: Decision +- activation-mode: sequential +""" + + assert "activation-mode: parallel" in task_section(plan, "Review", "Intake") + assert "activation-mode: sequential" in task_section(plan, "Review", "Decision") + with pytest.raises(SystemExit, match="ambiguous tasks.md T-entry"): + task_section(plan, "Review") + + +def test_sla_table_title_does_not_replace_canonical_stage_sla_field(): + shorthand = """ +#### Stage SLA +| SLA | At-Risk Display Name | +|---|---| +| 2 d | Intake SLA | +""" + + with pytest.raises(SystemExit, match="must declare exactly"): + check_canonical_stage_sla(shorthand, "Intake") From 07c4636982c2144945c3ed1702aff9331f2a7f20 Mon Sep 17 00:00:00 2001 From: abhiram-vad Date: Tue, 4 Aug 2026 07:16:08 -0700 Subject: [PATCH 2/5] fix(maestro-case): preserve authored rule contracts --- skills/uipath-maestro-case/SKILL.md | 2 +- .../references/phase-0-interview.md | 2 +- .../references/planning.md | 8 +- .../task-entry-conditions/planning.md | 4 +- .../test_reentry_reachability_checks.py | 22 ++++- .../athena_cm_event/athena_cm_event.yaml | 11 ++- .../check_athena_cm_event_plan.py | 93 +++++++++++++++++++ .../athena_cm_event/test_checkers.py | 38 ++++++++ .../check_picker_pairing.py | 39 ++++---- .../finalize_picker_pairing.yaml | 17 +++- .../test_check_picker_pairing.py | 92 ++++++++++++++++++ 11 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py create mode 100644 tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py diff --git a/skills/uipath-maestro-case/SKILL.md b/skills/uipath-maestro-case/SKILL.md index e81967f480..87ea0b197d 100644 --- a/skills/uipath-maestro-case/SKILL.md +++ b/skills/uipath-maestro-case/SKILL.md @@ -33,7 +33,7 @@ When `sdd.md` is absent, **Phase 0** designs the case by best assumption from th 3. **PHASE 1 HARD GATE — fresh registry before planning, pulled at most once per session.** Run `uip login status --output json`, then `uip maestro case registry pull`, before cache inspection, carryover reuse, resource resolution, or any Phase 1 artifact write — **same-session fast path:** when Phase 0's pull already succeeded in THIS session and `sdd.md` was just rendered from the confirmed in-memory model, reuse that cache and skip the re-pull. Any doubt runs the gate in full: user-provided SDD, cross-session resume, context compaction, a Phase 0 pull that failed or never ran, or missing cache files. **Plan-only exception:** if the user explicitly asks to stop at `sdd.md`/`sdd.draft.md`/`tasks.md` and not create `caseplan.json`, do not run tenant registry, connection, schema, or user-discovery commands; preserve concrete intended resource/system names, mark identities `resolve at build`, and report that resource wiring is deferred to the later build run. Trust the SDD as written; the pull refreshes the local discovery cache and does not validate or override the SDD. **Cache-state rule:** before a successful pull (this session), a missing cache directory/file is a failed refresh precondition — never a zero-match result. Only after a successful pull may an empty exact-name match set (or a still-absent type index) enter the normal empty-lookup flow. Login/pull failure → surface it and stop Phase 1. Discovery reads `~/.uip/case-resources/-index.json` directly because `registry search` has known gaps (esp. action-apps). Phase 0 pulls lazily only for build runs: the same login/pull chain starts in the background only when the case first shows tenant-bound work and a later build may need identities, followed by one light name-match pass — no schema discovery, no resource prompts; unclear items defer to this gate as `resolve at build`. See [references/registry-discovery.md](references/registry-discovery.md). 4. **`--output json` on every parsed read.** 5. **Follow plugin per node type.** Open matching `planning.md` during planning + `impl-json.md` during execution. Never guess JSON shapes from memory. -6. **`tasks.md` declarative and lossless only.** No shell commands inside. Field names use plain identifiers (e.g., `type:`, `displayName:`, `lane:`), not CLI flag syntax. One T-entry per sdd.md declaration — every stage, task, trigger, condition, SLA rule, **variable, and argument** gets own T-number, even when value looks like default (`current-stage-entered`, `case-entered`, `exit-only`, `is-interrupting: false`, `runOnlyOnce: true`, `marks-stage-complete: true`). Never group, never silently omit. Preserve every stage/task/SLA `Design Rationale` and condition routing/activation rationale as `rationale:` on the matching T-entry; rationale is reviewer/audit context and never changes the executable JSON shape. Preserve every SDD Inputs row with its declared binding mode and value. A JSON object literal stays literal through both handoffs: record the exact JSON in `tasks.md`, then write either the native object or its JSON-encoded string to `input.value`; never add `=js:` or `=jsonString:` unless the SDD itself explicitly uses that prefix. Project every task/rule Outputs table row through the common grammar in [`plugins/variables/io-binding/planning.md`](references/plugins/variables/io-binding/planning.md#sdd-outputs-table-to-tasksmd-projection-mandatory), then preserve each resulting `outputs:` item **with its operator and both operands unchanged**. SDD Outputs rows require `->` or `=`; a bare `tasks.md` output is generated only from resolved-schema discovery and is never authored as an SDD row. SDD table placeholders such as a `—` Field are not operands and never appear in `tasks.md`. In particular, `greeting -> greeting` is NOT equivalent to schema-discovered bare `greeting`: the former extracts into the predeclared case variable and requires `originalVar`; the latter auto-mints a task-local output. Never simplify an equal-name `->` row. **When an sdd.md row's format is unrecognized, ambiguous, or cannot be categorized — invoke AskUserQuestion before skipping. Silent omission is forbidden.** Always regenerate from scratch (greenfield/planning only — brownfield targeted edits mutate in place and preserve IDs; see [references/brownfield.md](references/brownfield.md)). **Every §4.6 task T-entry carries its own `activation-mode:` and `entry-rule:` lines — a separate §4.7 `rule-type:` entry does not satisfy this.** **Every task T-entry heading quotes the task's display name** in the exact form `## T: Add task "" to ""` (e.g. `## T08: Add wait-for-timer task "First Step" to "Process"`) — an unquoted or reworded heading (e.g. `## T08: Task First Step`) breaks plan addressability and fails plan validators even when `caseplan.json` itself is correct. See [`references/planning.md` §4.0](references/planning.md) and the [Plan-shape gate](references/planning.md#step-5--finalize-tasksmd-auto-proceed-to-phase-2). +6. **`tasks.md` declarative and lossless only.** No shell commands inside. Field names use plain identifiers (e.g., `type:`, `displayName:`, `lane:`), not CLI flag syntax. One T-entry per sdd.md declaration — every stage, task, trigger, condition, SLA rule, **variable, and argument** gets own T-number, even when value looks like default (`current-stage-entered`, `case-entered`, `exit-only`, `is-interrupting: false`, `runOnlyOnce: true`, `marks-stage-complete: true`). Never group, never silently omit. **An explicit stage/task entry or exit rule in a supplied or approved SDD is authoritative: planning and implementation preserve that exact rule and its selectors, even when a different rule would normally be inferred from task proximity or list order.** Preserve every stage/task/SLA `Design Rationale` and condition routing/activation rationale as `rationale:` on the matching T-entry; rationale is reviewer/audit context and never changes the executable JSON shape. Preserve every SDD Inputs row with its declared binding mode and value. A JSON object literal stays literal through both handoffs: record the exact JSON in `tasks.md`, then write either the native object or its JSON-encoded string to `input.value`; never add `=js:` or `=jsonString:` unless the SDD itself explicitly uses that prefix. Project every task/rule Outputs table row through the common grammar in [`plugins/variables/io-binding/planning.md`](references/plugins/variables/io-binding/planning.md#sdd-outputs-table-to-tasksmd-projection-mandatory), then preserve each resulting `outputs:` item **with its operator and both operands unchanged**. SDD Outputs rows require `->` or `=`; a bare `tasks.md` output is generated only from resolved-schema discovery and is never authored as an SDD row. SDD table placeholders such as a `—` Field are not operands and never appear in `tasks.md`. In particular, `greeting -> greeting` is NOT equivalent to schema-discovered bare `greeting`: the former extracts into the predeclared case variable and requires `originalVar`; the latter auto-mints a task-local output. Never simplify an equal-name `->` row. **When an sdd.md row's format is unrecognized, ambiguous, or cannot be categorized — invoke AskUserQuestion before skipping. Silent omission is forbidden.** Always regenerate from scratch (greenfield/planning only — brownfield targeted edits mutate in place and preserve IDs; see [references/brownfield.md](references/brownfield.md)). **Every §4.6 task T-entry carries its own `activation-mode:` and `entry-rule:` lines — a separate §4.7 `rule-type:` entry does not satisfy this.** **Every task T-entry heading quotes the task's display name** in the exact form `## T: Add task "" to ""` (e.g. `## T08: Add wait-for-timer task "First Step" to "Process"`) — an unquoted or reworded heading (e.g. `## T08: Task First Step`) breaks plan addressability and fails plan validators even when `caseplan.json` itself is correct. See [`references/planning.md` §4.0](references/planning.md) and the [Plan-shape gate](references/planning.md#step-5--finalize-tasksmd-auto-proceed-to-phase-2). 7. **`tasks.md` gate — auto-approved by default, opt-in stop.** Phase 1 auto-proceeds into Phase 2 Prototyping with no AskUserQuestion sign-off; treat the plan as approved. **Stop after `tasks.md` only when the request explicitly asked for a plan-only / review-first run** (e.g. "just the plan", "Phase 1 only", "stop after tasks.md for review", "don't build the case yet") — then report the plan and do NOT proceed to Phase 2. Re-read `tasks.md` before executing. 8. **Unresolved resource → placeholder, never fabricate IDs.** Keep `` markers in `tasks.md`. Placeholder **task**: node with `type` + `displayName` + structural fields, `data: {}`; conditions still reference the TaskId. Placeholder **event trigger**: node with render fields + `data.uipath: { serviceType: "Intsvc.EventTrigger" }` only (no other `data.uipath` keys); `entry-points.json` entry appended. No trigger-edge is created (Rule 20). See [references/placeholder-tasks.md](references/placeholder-tasks.md) and [references/plugins/triggers/event/impl-json.md § Placeholder fallback](references/plugins/triggers/event/impl-json.md). 9. **Persist every registry resolution to `registry-resolved.json`** — one object per task with exact keys `stage`, `task`, `taskType`, `cacheFile`, `searchQuery`, `matches`, `selected`, and `rationale` (plus resolved I/O/review metadata when applicable). `stage` + `task` associate the audit entry to one SDD declaration; `cacheFile` is the basename actually searched; `matches` is the full exact-name match set from the cache refreshed in Rule 3, not a summary. Use the authoritative SDD fields as the search and selection contract; record `selected` from that match set, or `null` after a genuine empty lookup. diff --git a/skills/uipath-maestro-case/references/phase-0-interview.md b/skills/uipath-maestro-case/references/phase-0-interview.md index 1166206cd5..3667c9fbe3 100644 --- a/skills/uipath-maestro-case/references/phase-0-interview.md +++ b/skills/uipath-maestro-case/references/phase-0-interview.md @@ -215,7 +215,7 @@ Generation: Read [`assets/templates/sdd-viewer.html`](../assets/templates/sdd-vi If the user explicitly asks to finalize the existing draft, choose `Use the draft — finalize and continue` by assumption and do not ask a redundant resumption question. If AskUserQuestion is unavailable, make the same assumption unless the user asked to discard or abort. Finalization stays inside this skill: render the final `sdd.md` from the Case Management template and run the template conformance gate; never route `sdd.draft.md` finalization to `uipath-planner`. -**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure only: inventory the draft's stage and task headings in memory, then render one complete output block for each; never use `cp`, `mv`, `install`, `rsync`, or another shell copy/rename operation to turn the draft into the final artifact. Every existing stage gets `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`. Every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Use concise default detail tables when the draft has only task summaries, but preserve exact stage and task display names (including punctuation), task types, variables, conditions, connector placeholders, and domain rules; structural normalization never renames business elements. A thresholded actor in draft prose/personas must also become executable inside an existing task — use a guarded owner/recipient/assignment expression that names the threshold and actor on the same line (for example, `=js:vars.loanAmount > 5000000 ? "Role:CreditAnalyst" : "Role:Underwriter"`); persona prose alone is not final, and this normalization must not add or rename a task. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. For a large draft that needs batched writes, first Write the complete ordered document skeleton — Sections 1–4 and every primary/secondary stage heading in source order inside Section 2 — then Edit each stage/task block in place. Never append a deferred or omitted stage after `## Section 3`; insert it at its existing Section 2 heading before continuing. Before writing, confirm the output has the same ordered stage/task inventory and that every stage/task block carries its required literal markers: stage `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`; task `**Activation Mode:**`, `**Design Rationale:**`, `**Task envelope**`, and the matching type-specific detail heading. Section 2 is incomplete until every inventoried stage and task appears before `## Section 3`. Then write `sdd.md` with Write/Edit and stop. +**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure and repair mechanically required rule pairings only: a schema-required companion rule is not a redesign. In particular, retain an authored `user-selected-stage` lane and give every eligible upstream primary stage a completing `required-tasks-completed` / `wait-for-user` / `Marks Stage Complete: Yes` exit; wording such as "any active case" means every primary stage. `wait-for-user` is picker exposure, not automatic event/SLA/decision routing, so do not add any such trigger. Inventory the draft's stage and task headings in memory, then render one complete output block for each; never use `cp`, `mv`, `install`, `rsync`, or another shell copy/rename operation to turn the draft into the final artifact. Every existing stage gets `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`. Every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Use concise default detail tables when the draft has only task summaries, but preserve exact stage and task display names (including punctuation), task types, variables, conditions, connector placeholders, and domain rules; structural normalization never renames business elements. A thresholded actor in draft prose/personas must also become executable inside an existing task — use a guarded owner/recipient/assignment expression that names the threshold and actor on the same line (for example, `=js:vars.loanAmount > 5000000 ? "Role:CreditAnalyst" : "Role:Underwriter"`); persona prose alone is not final, and this normalization must not add or rename a task. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. For a large draft that needs batched writes, first Write the complete ordered document skeleton — Sections 1–4 and every primary/secondary stage heading in source order inside Section 2 — then Edit each stage/task block in place. Never append a deferred or omitted stage after `## Section 3`; insert it at its existing Section 2 heading before continuing. Before writing, confirm the output has the same ordered stage/task inventory and that every stage/task block carries its required literal markers: stage `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`; task `**Activation Mode:**`, `**Design Rationale:**`, `**Task envelope**`, and the matching type-specific detail heading. Section 2 is incomplete until every inventoried stage and task appears before `## Section 3`. Then write `sdd.md` with Write/Edit and stop. ## What to say while working diff --git a/skills/uipath-maestro-case/references/planning.md b/skills/uipath-maestro-case/references/planning.md index 54d733f2c8..51f20019b5 100644 --- a/skills/uipath-maestro-case/references/planning.md +++ b/skills/uipath-maestro-case/references/planning.md @@ -99,7 +99,7 @@ If the plan-only / no-build exception is active, skip registry and schema discov - Task entries: `stage`, `type`, `activation-mode`, `entry-rule`, `lane`, `required`, `run-only-once`, `resource-intent`, `identity: resolve at build`, `rationale`. - Trigger/condition/SLA entries: `rule-type`, `source/status`, `target stage/task`, `return-or-close behavior`, `rationale`. Every `selected-tasks-completed` entry carries `selected-tasks-ids`. -**Rule-valued fields take canonical values, never prose.** `activation-mode`, `entry-rule`, `exit-rule`, and `rule-type` carry a value from their vocabulary exactly as spelled (`runs-sequentially`, `current-stage-entered`, `wait-for-connector`, `adhoc`, `selected-tasks-completed`, …) — review-oriented does not mean free text. Writing `entry-rule: completed` for a task that follows its predecessor is the compact-mode spelling of a duplicate `selected-tasks-completed` gate and reintroduces the defect the task-set grouping exists to prevent: siblings after one predecessor say `runs-sequentially`, and the ordering lives in `lane` / task-set position. Put the human phrasing in `rationale`. +**Rule-valued fields take canonical values, never prose.** `activation-mode`, `entry-rule`, `exit-rule`, and `rule-type` carry a value from their vocabulary exactly as spelled (`runs-sequentially`, `current-stage-entered`, `wait-for-connector`, `adhoc`, `selected-tasks-completed`, …) — review-oriented does not mean free text. When the supplied/approved SDD has an explicit rule row, copy that rule and its selectors exactly; task proximity and list order never authorize planning to normalize it. Only derive `runs-sequentially` for ordered work whose source does not already declare an entry rule. Put the human phrasing in `rationale`. **`lane` is a number, and grouping is expressed by sharing it.** `lane` is the zero-based `data.tasks` task-set index — `lane: 2`, never a descriptive name like `lane: payment confirmation`. Tasks that run as one task set carry the **same** number: `parallel-after-predecessor` siblings after one predecessor share a single lane value, and a strict chain increments. Giving two siblings different lanes contradicts their `activation-mode` and emits them as separate task sets, which is the defect the mode exists to prevent — the label alone does not group them. @@ -327,13 +327,15 @@ Additional fields are plugin-specific; read the plugin's `planning.md` before fi > **Activation-mode audit before writing §4.7.** After §4.6 is drafted and before any condition T-entry is written, scan every stage's task list and make the task mode visible in the plan: > -> - Contiguous ordered work in one stage (`then`, `after`, `before`, `in order`, direct previous-step wording, or an upstream prerequisite) → every task in that ordered run gets `activation-mode: sequential` and `entry-rule: runs-sequentially`, including the first task. +> **Authority order:** an explicit rule in the supplied/approved SDD wins. This audit verifies the handoff; it does not redesign or normalize authored rules. Use the derivation bullets below only when authoring from source behavior that has no explicit task-entry rule. +> +> - Contiguous ordered work in one stage (`then`, `after`, `before`, `in order`, direct previous-step wording, or an upstream prerequisite) → every task in that ordered run gets `activation-mode: sequential` and `entry-rule: runs-sequentially`, including the first task, unless the SDD explicitly declares another legal rule. > - Independent work that starts with the stage → `activation-mode: parallel`, `entry-rule: current-stage-entered`, and rationale says why the tasks are independent. > - Connector/event callback wait → `activation-mode: event-triggered`, usually `entry-rule: wait-for-connector`. > - User-launched optional work → `activation-mode: adhoc`, `entry-rule: adhoc`, `isRequired: false`. > - Branch convergence, fan-in, decision-result routing, or a non-immediate dependency → `activation-mode: fan-in` or `conditional-gate`, `entry-rule: selected-tasks-completed`, with the selected tasks named. > -> A task whose only reason for `selected-tasks-completed` is "it follows the immediately previous task" is a planning defect. Convert that contiguous run to `runs-sequentially` instead. `selected-tasks-completed` remains correct for fan-in, branch convergence, non-immediate dependencies, and stage-exit routing conditions. +> While authoring a new SDD, do not invent `selected-tasks-completed` merely because a task follows the immediately previous task; model a plain contiguous run as `runs-sequentially`. Once an SDD is supplied or approved, however, preserve every explicit `selected-tasks-completed` row and selector in `tasks.md` and `caseplan.json`; planning is not a second design pass. Map that task to `conditional-gate` or `fan-in` as its authored rationale supports, never to `sequential`. > Before leaving §4.6, audit each stage's planned lanes: sequential tasks that form a strict chain MUST NOT share a lane with each other or with adhoc/event-driven/parallel work. If `activation-mode`/`entry-rule` conflicts with `lane`, the mode wins and the lane must be corrected. Same-lane grouping is reserved for intentionally parallel siblings, and the rationale must say why they run in parallel. > **Outputs are a lossless handoff, not a discovered-name summary.** Project each SDD Outputs table row through the common grammar in [`plugins/variables/io-binding/planning.md` § SDD Outputs table → `tasks.md` projection](plugins/variables/io-binding/planning.md#sdd-outputs-table-to-tasksmd-projection-mandatory), then preserve the resulting list item exactly. Schema discovery may add truly undeclared fields as bare items, but it must not rewrite an SDD row. An explicit equal-name extract such as `greeting -> greeting` stays exactly that; collapsing it to bare `greeting` changes the binding from "write the existing case variable" to "auto-mint a task output." Before the Step 5 approval gate, compare every SDD Outputs row to its task T-entry and fix any missing or changed operator/operand or leaked table placeholder. diff --git a/skills/uipath-maestro-case/references/plugins/conditions/task-entry-conditions/planning.md b/skills/uipath-maestro-case/references/plugins/conditions/task-entry-conditions/planning.md index 152c3fadf7..5ebc66be4e 100644 --- a/skills/uipath-maestro-case/references/plugins/conditions/task-entry-conditions/planning.md +++ b/skills/uipath-maestro-case/references/plugins/conditions/task-entry-conditions/planning.md @@ -53,7 +53,7 @@ The Case App selector has three distinct modes: `adhoc` is task-entry-only. It is never a stage entry rule, never a case trigger, never a substitute for `wait-for-connector`, and never the way to model a user-selected interrupting lane. Use a secondary stage with `user-selected-stage` for that. -For generated SDDs, any requirement that says `then`, `after`, `before`, `in order`, or otherwise declares an immediate dependency should already be authored as `runs-sequentially` on every task in that run. Do not convert it to parallel `current-stage-entered` tasks merely because no data binding links them. Use parallel mode only when the SDD rationale states that the tasks are independent. If a task row says `selected-tasks-completed("")`, preserve it only when the SDD is intentionally expressing a condition/event-driven sibling gate, branch convergence, or non-immediate dependency. +While authoring a new SDD, any requirement that says `then`, `after`, `before`, `in order`, or otherwise declares an immediate dependency should be authored as `runs-sequentially` on every task in that run. Do not convert it to parallel `current-stage-entered` tasks merely because no data binding links them. Use parallel mode only when the rationale says the tasks are independent. **Phase 1 does not re-author a supplied or approved SDD:** if its task row explicitly says `selected-tasks-completed("")`, preserve that exact rule and selector even when the selected task is immediately previous. ## Phase 1 Plan Presentation Contract @@ -77,7 +77,7 @@ For every task-entry-condition T-entry, verify the task's `activation-mode` and | `fan-in` | `selected-tasks-completed` with multiple selected tasks or an explicit convergence rationale | | `conditional-gate` | `selected-tasks-completed` with a branch/non-immediate dependency rationale, or the explicitly authored gate rule | -If the selected task is the immediately previous task in the same stage and there is no fan-in, branch, event, or non-immediate dependency rationale, `selected-tasks-completed` is a planning error. Rewrite the ordered run as `activation-mode: sequential` with `rule-type: runs-sequentially` on every task in the run. This is required even when all tasks are placeholders. +During Phase 0 authoring, a plain immediate ordered run with no fan-in, branch, event, or non-immediate dependency rationale should be modeled as `activation-mode: sequential` with `rule-type: runs-sequentially`. During Phase 1, never use that heuristic to rewrite an explicit supplied/approved SDD row: preserve `selected-tasks-completed` and its selector as `conditional-gate` or `fan-in`, including when all tasks are placeholders. ## Ordering diff --git a/tests/tasks/uipath-maestro-case/_shared/test_reentry_reachability_checks.py b/tests/tasks/uipath-maestro-case/_shared/test_reentry_reachability_checks.py index 6204321eca..50eb4cf6c2 100644 --- a/tests/tasks/uipath-maestro-case/_shared/test_reentry_reachability_checks.py +++ b/tests/tasks/uipath-maestro-case/_shared/test_reentry_reachability_checks.py @@ -73,11 +73,12 @@ def stage( COMPLETION_EXIT = ["`required-tasks-completed`", "—", "exit-only", "Yes"] PICKER_ENTRY = ["`user-selected-stage`", "—", "Yes"] -WAIT_FOR_USER_EXIT = ["`required-tasks-completed`", "—", "wait-for-user", "No"] +WAIT_FOR_USER_EXIT = ["`required-tasks-completed`", "—", "wait-for-user", "Yes"] def picker_sdd( *, + document_exits: list[list[str]] | None = None, upstream_exits: list[list[str]] | None = None, lane_entry: list[list[str]] | None = None, lane_exits: list[list[str]] | None = None, @@ -87,6 +88,11 @@ def picker_sdd( parts = [ "# SDD — VendorOnboarding\n", "## Section 2: Stages & Tasks\n", + stage( + "Document Collection", + [["`case-entered`", "—", "No"]], + document_exits or [COMPLETION_EXIT], + ), stage("Vendor Approval", [["`case-entered`", "—", "No"]], upstream_exits or [COMPLETION_EXIT]), ] if include_lane: @@ -236,8 +242,13 @@ def assert_fail(self, sdd_text: str, expected: str) -> None: self.assertNotEqual(result.returncode, 0, "grader accepted a regression") self.assertIn(expected, (result.stdout + result.stderr).lower()) - def test_accepts_picker_entry_paired_with_upstream_wait_for_user_exit(self) -> None: - self.assert_pass(picker_sdd(upstream_exits=[COMPLETION_EXIT, WAIT_FOR_USER_EXIT])) + def test_accepts_picker_entry_exposed_from_every_primary_stage(self) -> None: + self.assert_pass( + picker_sdd( + document_exits=[WAIT_FOR_USER_EXIT], + upstream_exits=[WAIT_FOR_USER_EXIT], + ) + ) def test_rejects_picker_entry_with_no_wait_for_user_anywhere(self) -> None: self.assert_fail(picker_sdd(), "wait-for-user") @@ -277,6 +288,11 @@ def test_wait_for_user_on_an_exception_lane_does_not_expose_the_picker_lane(self [ "# SDD — VendorOnboarding", "## Section 2: Stages & Tasks", + stage( + "Document Collection", + [["`case-entered`", "—", "No"]], + [COMPLETION_EXIT], + ), "### Stage 1: Vendor Approval", "**Type:** Stage", "#### Stage Entry Conditions", diff --git a/tests/tasks/uipath-maestro-case/athena_cm_event/athena_cm_event.yaml b/tests/tasks/uipath-maestro-case/athena_cm_event/athena_cm_event.yaml index 7f56eff122..dfdccd93e1 100644 --- a/tests/tasks/uipath-maestro-case/athena_cm_event/athena_cm_event.yaml +++ b/tests/tasks/uipath-maestro-case/athena_cm_event/athena_cm_event.yaml @@ -26,7 +26,8 @@ initial_prompt: | Use the staged `sdd.md` as the sole source of truth. Do not redo Phase 0, do not re-interview me, and do not redesign the process. The document's fixture is limited to the supported caseplan topology; generate only the - case plan. + case plan. Preserve every authored stage and task entry/exit rule exactly; + planning must not normalize an explicit SDD rule into a different mode. Important: - The `uip` CLI is already available and authenticated in the environment. @@ -76,6 +77,14 @@ success_criteria: weight: 2.0 pass_threshold: 1.0 + - type: run_command + description: "tasks.md preserves the authored Athena task-entry rules before JSON generation" + command: "python3 $TASK_DIR/check_athena_cm_event_plan.py" + timeout: 30 + expected_exit_code: 0 + weight: 3.0 + pass_threshold: 1.0 + - type: run_command description: "generated caseplan preserves the Athena stage/task rules, external identity, and event trigger" command: "python3 $TASK_DIR/check_athena_cm_event_case.py" diff --git a/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py b/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py new file mode 100644 index 0000000000..4fa9181c8c --- /dev/null +++ b/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Assert that Phase 1 does not reinterpret Athena's authored task-entry rules.""" + +from __future__ import annotations + +import glob +import re +import sys + + +EXPECTED_RULES = { + "StageATask1": "current-stage-entered", + "StageATask2": "selected-tasks-completed", + "StageBTask1": "current-stage-entered", + "StageBTask2": "current-stage-entered", + "StageCTask1": "current-stage-entered", + "StageCTask2": "current-stage-entered", + "StageCTask3": "selected-tasks-completed", +} +SELECTED_PREDECESSORS = { + "StageATask2": "StageATask1", + "StageCTask3": "StageCTask2", +} + + +def fail(message: str) -> None: + sys.exit(f"FAIL: {message}") + + +def read_plan() -> str: + matches = sorted( + path + for path in glob.glob("**/tasks/tasks.md", recursive=True) + if "/.venv/" not in path + ) + if len(matches) != 1: + fail(f"expected one tasks/tasks.md, found {matches}") + return open(matches[0], encoding="utf-8").read() + + +def task_section(plan: str, task_name: str) -> str: + match = re.search( + rf'(?ims)^##\s+T\d+:[^\n]*"{re.escape(task_name)}"[^\n]*\n' + rf".*?(?=^##\s+T\d+:|\Z)", + plan, + ) + if not match: + fail(f"tasks.md has no quoted T-entry for {task_name!r}") + return match.group(0) + + +def field(section: str, name: str, task_name: str) -> str: + match = re.search( + rf"(?im)^-\s*(?:\*\*)?{re.escape(name)}:(?:\*\*)?\s*`?([a-z][a-z0-9-]*)`?\s*$", + section, + ) + if not match: + fail(f"{task_name} is missing {name} in its own task T-entry") + return match.group(1).lower() + + +def main() -> None: + plan = read_plan() + for task_name, expected_rule in EXPECTED_RULES.items(): + section = task_section(plan, task_name) + actual_rule = field(section, "entry-rule", task_name) + mode = field(section, "activation-mode", task_name) + if actual_rule != expected_rule: + fail( + f"{task_name} authored entry-rule is {expected_rule}, but tasks.md changes it " + f"to {actual_rule} ({mode})" + ) + if expected_rule == "current-stage-entered" and mode != "parallel": + fail(f"{task_name} must preserve parallel/current-stage-entered, got {mode}/{actual_rule}") + if expected_rule == "selected-tasks-completed" and mode not in { + "conditional-gate", + "fan-in", + }: + fail( + f"{task_name} must preserve its explicit selected-task gate, got " + f"{mode}/{actual_rule}" + ) + + for task_name, predecessor in SELECTED_PREDECESSORS.items(): + section = task_section(plan, task_name) + if predecessor not in section: + fail(f"{task_name} selected-task gate does not name predecessor {predecessor}") + + print("OK: Athena tasks.md preserves every authored task-entry rule") + + +if __name__ == "__main__": + main() diff --git a/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py b/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py index e8b731dade..ec5c36bcb0 100644 --- a/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py +++ b/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py @@ -13,6 +13,7 @@ ROOT = Path(__file__).parent SDD_CHECK = ROOT / "check_athena_cm_event_sdd.py" +PLAN_CHECK = ROOT / "check_athena_cm_event_plan.py" CASE_CHECK = ROOT / "check_athena_cm_event_case.py" @@ -206,6 +207,32 @@ def write_caseplan(self, *, stage_c_task_3_once: bool = True) -> None: caseplan.parent.mkdir(parents=True) caseplan.write_text(json.dumps(plan), encoding="utf-8") + def write_tasks_md(self, *, rewrite_a2_as_sequential: bool = False) -> None: + contracts = { + "StageATask1": ("parallel", "current-stage-entered", None), + "StageATask2": ( + "sequential" if rewrite_a2_as_sequential else "conditional-gate", + "runs-sequentially" if rewrite_a2_as_sequential else "selected-tasks-completed", + None if rewrite_a2_as_sequential else "StageATask1", + ), + "StageBTask1": ("parallel", "current-stage-entered", None), + "StageBTask2": ("parallel", "current-stage-entered", None), + "StageCTask1": ("parallel", "current-stage-entered", None), + "StageCTask2": ("parallel", "current-stage-entered", None), + "StageCTask3": ("conditional-gate", "selected-tasks-completed", "StageCTask2"), + } + sections = [] + for index, (task_name, (mode, rule, selected)) in enumerate(contracts.items(), 1): + selected_line = f"\n- selected-tasks-ids: {selected}" if selected else "" + sections.append( + f'## T{index}: Add process task "{task_name}" to "Stage"\n\n' + f"- activation-mode: {mode}\n" + f"- entry-rule: {rule}{selected_line}\n" + ) + tasks = self.workdir / "tasks" / "tasks.md" + tasks.parent.mkdir() + tasks.write_text("\n".join(sections), encoding="utf-8") + def test_sdd_checker_accepts_complete_fixture(self) -> None: self.write_sdd(self.workdir / "sdd.md") result = run(SDD_CHECK, self.workdir) @@ -216,6 +243,17 @@ def test_sdd_checker_accepts_topology_without_external_router(self) -> None: result = run(SDD_CHECK, self.workdir) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_plan_checker_accepts_authored_task_entry_rules(self) -> None: + self.write_tasks_md() + result = run(PLAN_CHECK, self.workdir) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_plan_checker_rejects_rewriting_selected_dependency_as_sequential(self) -> None: + self.write_tasks_md(rewrite_a2_as_sequential=True) + result = run(PLAN_CHECK, self.workdir) + self.assertNotEqual(result.returncode, 0) + self.assertIn("StageATask2", result.stdout + result.stderr) + def test_case_checker_accepts_expected_structure(self) -> None: self.write_caseplan() result = run(CASE_CHECK, self.workdir) diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py index dd578f9f7a..6c32f5d7d7 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py @@ -31,6 +31,7 @@ ) LANE = "Compliance Hold" +PRIMARY_STAGES = ("Document Collection", "Vendor Approval") def main() -> None: @@ -54,23 +55,29 @@ def main() -> None: if not interrupting.startswith("y"): fail(f"{LANE!r} user-selected-stage entry row is not Interrupting: Yes (got {interrupting!r})") - # The pairing: an upstream PRIMARY stage must expose this lane via a wait-for-user exit. - # A secondary or exception lane exposing another lane is not an upstream producer. + # The requirement says the person may pull *any active case* into the lane, + # so every named primary stage must expose the picker with the canonical + # completing exit. Exposure from one phase cannot cover another active phase. exposing = [] - for label, block in blocks.items(): - if label.lower() == LANE.lower() or stage_kind(block) != "primary": - continue - for row in exit_rows(block): - if "wait-for-user" in column(row, "exit type").lower(): - exposing.append(label) - if not exposing: - fail( - f"{LANE!r} is entered by user-selected-stage but no upstream stage carries a " - "wait-for-user exit, so nothing exposes the lane to the picker and it is unreachable " - "(sdd-generation-rules § Logical integrity 5, § Finalization 12a)" - ) - - print(f"OK: {LANE} user-selected-stage entry is paired with a wait-for-user exit on {exposing}") + for label in PRIMARY_STAGES: + block = find_stage(blocks, label) + if stage_kind(block) != "primary": + fail(f"{label!r} must remain a primary stage") + valid_rows = [ + row + for row in exit_rows(block) + if rule_type(row) == "required-tasks-completed" + and "wait-for-user" in column(row, "exit type").lower() + and column(row, "marks stage complete").lower().startswith("y") + ] + if not valid_rows: + fail( + f"{label!r} must expose {LANE!r} with a required-tasks-completed / " + "wait-for-user / Marks Stage Complete: Yes exit" + ) + exposing.append(label) + + print(f"OK: {LANE} user-selected-stage entry is exposed from every primary stage: {exposing}") if __name__ == "__main__": diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml index f1a2fe7d65..b3ba92db6a 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml @@ -34,6 +34,10 @@ initial_prompt: | Management SDD template; do not route it through cross-product planning. Use the direct finalization path: read the draft and SDD template, write the finalized `sdd.md`, and do not spawn subagents or inspect planning/plugin docs. + Repair mechanically required rule pairings without changing business intent: + a manual stage-picker lane needs `wait-for-user` exposure from every primary + stage where it may be selected. That exposure is not automatic event, SLA, + or decision routing. success_criteria: - type: file_exists @@ -43,13 +47,21 @@ success_criteria: pass_threshold: 1.0 - type: run_command - description: "Compliance Hold keeps its user-selected-stage entry AND an upstream stage exposes it through a wait-for-user exit" + description: "Compliance Hold keeps its user-selected-stage entry and every eligible primary stage exposes it through wait-for-user" command: "python3 $TASK_DIR/check_picker_pairing.py" timeout: 30 expected_exit_code: 0 weight: 3.0 pass_threshold: 1.0 + - type: run_command + description: "finalized sdd.md passes the shared mechanical SDD contract" + command: "python3 $SKILLS_REPO_PATH/tests/tasks/uipath-maestro-case/_shared/sdd_check.py" + timeout: 30 + expected_exit_code: 0 + weight: 3.0 + pass_threshold: 1.0 + - type: run_command description: "stopped at the finalized sdd.md — did not run ahead into a caseplan build" command: "! find . -name caseplan.json -not -path '*/.venv/*' | grep -q ." @@ -68,8 +80,9 @@ simulation: Get sdd.draft.md finalized into an approved sdd.md, then stop. constraints: - "If asked how to proceed with the existing draft, say to use it as-is and finalize it — you do not want to be re-interviewed." - - "Approve the finalized sdd.md on the first pass. Approve anyway if it warns about the unresolved api-workflow identity or raises review items." + - "Approve the finalized sdd.md on the first pass only after the manual picker lane has its required wait-for-user exposure; the unresolved api-workflow identity is still acceptable." - "Confirm that the Compliance Officer launches Compliance Hold by hand from the stage picker; there is no event, SLA, or decision button that routes there." + - "If asked about wait-for-user, confirm it is the required picker exposure and does not add automatic routing." - "Do not request design changes or add requirements." - "Keep the agent in Phase 0. If it offers to plan tasks.md or build a caseplan, tell it to stop at the approved sdd.md." - "Keep replies to one sentence." diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py new file mode 100644 index 0000000000..9ead2a0bd1 --- /dev/null +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Behavioral tests for the manual stage-picker pairing grader.""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +CHECKER = Path(__file__).parent / "check_picker_pairing.py" +PRIMARY_STAGES = ("Document Collection", "Vendor Approval") + + +def run(cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(CHECKER)], + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +class PickerPairingCheckerTest(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.workdir = Path(self.temporary.name) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def write_sdd( + self, + exposed_by: tuple[str, ...], + *, + noncompleting: tuple[str, ...] = (), + ) -> None: + primary_blocks = [] + for index, stage_name in enumerate(PRIMARY_STAGES, 1): + exit_type = "wait-for-user" if stage_name in exposed_by else "exit-only" + marks_complete = "No" if stage_name in noncompleting else "Yes" + primary_blocks.append( + f"""### Stage {index}: {stage_name} + +#### Stage Exit Conditions + +| WHEN | IF | Exit Type | Marks Stage Complete | +|---|---|---|---| +| required-tasks-completed | — | {exit_type} | {marks_complete} | +""" + ) + text = "# SDD — VendorOnboarding\n\n" + "\n".join(primary_blocks) + """ +### Secondary Stage: Compliance Hold + +#### Stage Entry Conditions + +| WHEN | IF | Interrupting | +|---|---|---| +| user-selected-stage | — | Yes | + +#### Stage Exit Conditions + +| WHEN | IF | Exit Type | Marks Stage Complete | +|---|---|---|---| +| required-tasks-completed | — | return-to-origin | Yes | +""" + (self.workdir / "sdd.md").write_text(text, encoding="utf-8") + + def test_accepts_picker_exposure_from_every_primary_stage(self) -> None: + self.write_sdd(PRIMARY_STAGES) + result = run(self.workdir) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_rejects_picker_exposure_from_only_one_primary_stage(self) -> None: + self.write_sdd(("Document Collection",)) + result = run(self.workdir) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Vendor Approval", result.stdout + result.stderr) + + def test_rejects_noncompleting_wait_for_user_exit(self) -> None: + self.write_sdd(PRIMARY_STAGES, noncompleting=("Vendor Approval",)) + result = run(self.workdir) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Vendor Approval", result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() From 7b3018a8106a85eb28505aa47d38865332089676 Mon Sep 17 00:00:00 2001 From: abhiram-vad Date: Tue, 4 Aug 2026 09:08:56 -0700 Subject: [PATCH 3/5] fix(maestro-case): enforce picker exit replacement --- skills/uipath-maestro-case/SKILL.md | 2 +- .../assets/templates/sdd-template.md | 1 + .../references/phase-0-interview.md | 2 +- .../athena_cm_event/check_athena_cm_event_plan.py | 3 ++- .../athena_cm_event/test_checkers.py | 4 ++-- .../check_picker_pairing.py | 15 +++++++++++---- .../finalize_picker_pairing.yaml | 4 +++- .../test_check_picker_pairing.py | 14 +++++++++++++- 8 files changed, 34 insertions(+), 11 deletions(-) diff --git a/skills/uipath-maestro-case/SKILL.md b/skills/uipath-maestro-case/SKILL.md index 87ea0b197d..6becfa1fb1 100644 --- a/skills/uipath-maestro-case/SKILL.md +++ b/skills/uipath-maestro-case/SKILL.md @@ -28,7 +28,7 @@ When `sdd.md` is absent, **Phase 0** designs the case by best assumption from th ## Critical Rules -1. **Phase 0 best-assumption design when `sdd.md` absent.** Listen and ground, then decide every open field per the assumption playbook — inform, don't interrogate: every assumption, override, and resource decision is disclosed in the single confirmation's `Decisions I Made` table. The confirmation is a decision-first **Case Review** with exactly eight sections: Case Snapshot, Primary Journey, Other Paths Considered, SLA and Escalations, Rules and Outcomes, Resources and Integrations, Decisions I Made, and Review Flags. It names every stage and task with task type, activation/grouping, required status, routing/outcome, and SLA context; it deliberately omits the data contract, variables, and task inputs/outputs, which remain complete in `sdd.md`. It must be complete enough to approve the business behavior without opening `sdd.md`; do not defer a missing business decision by saying it will be in the document. This structured Case Review is the only valid plan-first approval surface for Phase 0; a generic "Build Plan" / "Approve this plan" checkpoint does not count, and a user "Yes" to that checkpoint is not a Build answer. Question budget: one clarifying call (only for an empty request, contradictory inputs, user-requested questions, or no source signal for other paths) plus ONE confirmation ([phase-0-interview.md § Confirm](references/phase-0-interview.md#confirm--the-single-checkpoint)). If `sdd.draft.md` exists and the user asks to finalize it, use the direct draft-resumption path in this skill: read the draft and SDD template, render the final `sdd.md` from the Case Management template, do not spawn subagents, do not preload planning/plugin references, and never delegate to `uipath-planner`. On a Build answer, render `sdd.md` from the in-memory model batched with the first build actions — the file MUST pass the template-conformance gate in `phase-0-interview.md`; a summary SDD is invalid even if `caseplan.json` later validates. Explicit sign-off requests add one approval prompt; design-only requests save `sdd.md` and stop; draft requests save `sdd.draft.md` and stop. When the prompt explicitly says to get/save a draft and stop, that request is already the save instruction: show the Case Review, write `sdd.draft.md`, and stop without asking for another approval. When the prompt explicitly asks to produce `sdd.md` plus `tasks/tasks.md` and stop before `caseplan.json`, use the bounded no-build fast path in `phase-0-interview.md`: after the Case Review, write the full-template `sdd.md`, create `tasks/`, write compact `tasks/tasks.md`, and stop; do not read planning/plugin references, tenant discovery sources, or the full SDD finalization checklist. Never overwrite an existing `sdd.md`. +1. **Phase 0 best-assumption design when `sdd.md` absent.** Listen and ground, then decide every open field per the assumption playbook — inform, don't interrogate: every assumption, override, and resource decision is disclosed in the single confirmation's `Decisions I Made` table. The confirmation is a decision-first **Case Review** with exactly eight sections: Case Snapshot, Primary Journey, Other Paths Considered, SLA and Escalations, Rules and Outcomes, Resources and Integrations, Decisions I Made, and Review Flags. It names every stage and task with task type, activation/grouping, required status, routing/outcome, and SLA context; it deliberately omits the data contract, variables, and task inputs/outputs, which remain complete in `sdd.md`. It must be complete enough to approve the business behavior without opening `sdd.md`; do not defer a missing business decision by saying it will be in the document. This structured Case Review is the only valid plan-first approval surface for Phase 0; a generic "Build Plan" / "Approve this plan" checkpoint does not count, and a user "Yes" to that checkpoint is not a Build answer. Question budget: one clarifying call (only for an empty request, contradictory inputs, user-requested questions, or no source signal for other paths) plus ONE confirmation ([phase-0-interview.md § Confirm](references/phase-0-interview.md#confirm--the-single-checkpoint)). If `sdd.draft.md` exists and the user asks to finalize it, use the direct draft-resumption path in this skill: read the draft and SDD template, render the final `sdd.md` from the Case Management template, do not spawn subagents, do not preload planning/plugin references, and never delegate to `uipath-planner`. **Direct finalization repairs schema-required companion rules as row replacements, not extra routes: for an authored `user-selected-stage`, replace each eligible origin's existing `required-tasks-completed | exit-only | Yes` row with the single row `required-tasks-completed | wait-for-user | Yes`; never retain the old completion row or add a `Marks Stage Complete: No` duplicate.** `wait-for-user` exposes the picker; it does not add automatic event/SLA/decision routing. On a Build answer, render `sdd.md` from the in-memory model batched with the first build actions — the file MUST pass the template-conformance gate in `phase-0-interview.md`; a summary SDD is invalid even if `caseplan.json` later validates. Explicit sign-off requests add one approval prompt; design-only requests save `sdd.md` and stop; draft requests save `sdd.draft.md` and stop. When the prompt explicitly says to get/save a draft and stop, that request is already the save instruction: show the Case Review, write `sdd.draft.md`, and stop without asking for another approval. When the prompt explicitly asks to produce `sdd.md` plus `tasks/tasks.md` and stop before `caseplan.json`, use the bounded no-build fast path in `phase-0-interview.md`: after the Case Review, write the full-template `sdd.md`, create `tasks/`, write compact `tasks/tasks.md`, and stop; do not read planning/plugin references, tenant discovery sources, or the full SDD finalization checklist. Never overwrite an existing `sdd.md`. 2. **sdd.md is sole input post-Phase-0 — across sessions.** When user-provided, or in any later session, re-run, or staleness recovery (context compaction), trust `sdd.md` as written; the skill does not validate or gap-fill it. Within the session that just confirmed the design, the in-memory model that rendered `sdd.md` is the same content and drives the build directly ([phase-0-interview.md § Build start](references/phase-0-interview.md#build-start--sdd-written-alongside-the-build)) — do not re-read the just-written file. If a build-phase ambiguity arises, use AskUserQuestion — never infer silently. 3. **PHASE 1 HARD GATE — fresh registry before planning, pulled at most once per session.** Run `uip login status --output json`, then `uip maestro case registry pull`, before cache inspection, carryover reuse, resource resolution, or any Phase 1 artifact write — **same-session fast path:** when Phase 0's pull already succeeded in THIS session and `sdd.md` was just rendered from the confirmed in-memory model, reuse that cache and skip the re-pull. Any doubt runs the gate in full: user-provided SDD, cross-session resume, context compaction, a Phase 0 pull that failed or never ran, or missing cache files. **Plan-only exception:** if the user explicitly asks to stop at `sdd.md`/`sdd.draft.md`/`tasks.md` and not create `caseplan.json`, do not run tenant registry, connection, schema, or user-discovery commands; preserve concrete intended resource/system names, mark identities `resolve at build`, and report that resource wiring is deferred to the later build run. Trust the SDD as written; the pull refreshes the local discovery cache and does not validate or override the SDD. **Cache-state rule:** before a successful pull (this session), a missing cache directory/file is a failed refresh precondition — never a zero-match result. Only after a successful pull may an empty exact-name match set (or a still-absent type index) enter the normal empty-lookup flow. Login/pull failure → surface it and stop Phase 1. Discovery reads `~/.uip/case-resources/-index.json` directly because `registry search` has known gaps (esp. action-apps). Phase 0 pulls lazily only for build runs: the same login/pull chain starts in the background only when the case first shows tenant-bound work and a later build may need identities, followed by one light name-match pass — no schema discovery, no resource prompts; unclear items defer to this gate as `resolve at build`. See [references/registry-discovery.md](references/registry-discovery.md). 4. **`--output json` on every parsed read.** diff --git a/skills/uipath-maestro-case/assets/templates/sdd-template.md b/skills/uipath-maestro-case/assets/templates/sdd-template.md index fa1453e0a9..e38022044f 100644 --- a/skills/uipath-maestro-case/assets/templates/sdd-template.md +++ b/skills/uipath-maestro-case/assets/templates/sdd-template.md @@ -54,6 +54,7 @@ build the case in the Case Designer without guessing. - `Marks Stage Complete: No` (routing / divergent exits) → WHEN may be `selected-tasks-completed("TaskA")`, `wait-for-connector`, etc. - Same stage may carry one completion exit (`Yes` + `required-tasks-completed` / `wait-for-connector`) plus zero or more routing exits (`No` + `selected-tasks-completed` / `wait-for-connector`). - `return-to-origin` is a completion exit: use `Marks Stage Complete: Yes` with `required-tasks-completed` (or `wait-for-connector`). Never pair it with `No` + `selected-tasks-completed`. + - **Stage-picker repair is a replacement, never a duplicate:** when `user-selected-stage` requires picker exposure from an origin, replace that origin's `required-tasks-completed | exit-only | Yes` completion row with `required-tasks-completed | wait-for-user | Yes`. Keep exactly one `required-tasks-completed` row; never add a second `Marks Stage Complete: No` row. *Case exit (preferred pattern: one row, `Yes` + `required-stages-completed`):* - `Marks Case Complete: Yes` → WHEN MUST be `required-stages-completed` or `wait-for-connector`. **NEVER** `selected-stage-completed(...)` / `selected-stage-exited(...)`. diff --git a/skills/uipath-maestro-case/references/phase-0-interview.md b/skills/uipath-maestro-case/references/phase-0-interview.md index 3667c9fbe3..47ad199b72 100644 --- a/skills/uipath-maestro-case/references/phase-0-interview.md +++ b/skills/uipath-maestro-case/references/phase-0-interview.md @@ -215,7 +215,7 @@ Generation: Read [`assets/templates/sdd-viewer.html`](../assets/templates/sdd-vi If the user explicitly asks to finalize the existing draft, choose `Use the draft — finalize and continue` by assumption and do not ask a redundant resumption question. If AskUserQuestion is unavailable, make the same assumption unless the user asked to discard or abort. Finalization stays inside this skill: render the final `sdd.md` from the Case Management template and run the template conformance gate; never route `sdd.draft.md` finalization to `uipath-planner`. -**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure and repair mechanically required rule pairings only: a schema-required companion rule is not a redesign. In particular, retain an authored `user-selected-stage` lane and give every eligible upstream primary stage a completing `required-tasks-completed` / `wait-for-user` / `Marks Stage Complete: Yes` exit; wording such as "any active case" means every primary stage. `wait-for-user` is picker exposure, not automatic event/SLA/decision routing, so do not add any such trigger. Inventory the draft's stage and task headings in memory, then render one complete output block for each; never use `cp`, `mv`, `install`, `rsync`, or another shell copy/rename operation to turn the draft into the final artifact. Every existing stage gets `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`. Every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Use concise default detail tables when the draft has only task summaries, but preserve exact stage and task display names (including punctuation), task types, variables, conditions, connector placeholders, and domain rules; structural normalization never renames business elements. A thresholded actor in draft prose/personas must also become executable inside an existing task — use a guarded owner/recipient/assignment expression that names the threshold and actor on the same line (for example, `=js:vars.loanAmount > 5000000 ? "Role:CreditAnalyst" : "Role:Underwriter"`); persona prose alone is not final, and this normalization must not add or rename a task. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. For a large draft that needs batched writes, first Write the complete ordered document skeleton — Sections 1–4 and every primary/secondary stage heading in source order inside Section 2 — then Edit each stage/task block in place. Never append a deferred or omitted stage after `## Section 3`; insert it at its existing Section 2 heading before continuing. Before writing, confirm the output has the same ordered stage/task inventory and that every stage/task block carries its required literal markers: stage `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`; task `**Activation Mode:**`, `**Design Rationale:**`, `**Task envelope**`, and the matching type-specific detail heading. Section 2 is incomplete until every inventoried stage and task appears before `## Section 3`. Then write `sdd.md` with Write/Edit and stop. +**Direct finalize fast path:** for a request that says the draft design is settled and asks for final `sdd.md` only, read `sdd.draft.md`, this resumption/gate section, and `assets/templates/sdd-template.md`; do not read planning/plugin references, do not inspect tenant resources, and do not spawn subagents. Treat the draft's stages, tasks, variables, conditions, SLAs, personas, and integration intent as the design source. Normalize structure and repair mechanically required rule pairings only: a schema-required companion rule is not a redesign. In particular, retain an authored `user-selected-stage` lane and give every eligible upstream primary stage a completing `required-tasks-completed` / `wait-for-user` / `Marks Stage Complete: Yes` exit; wording such as "any active case" means every primary stage. **This repair replaces that stage's existing `required-tasks-completed | exit-only | Yes` row; it never adds a second completion row or a `Marks Stage Complete: No` row.** `wait-for-user` is picker exposure, not automatic event/SLA/decision routing, so do not add any such trigger. Inventory the draft's stage and task headings in memory, then render one complete output block for each; never use `cp`, `mv`, `install`, `rsync`, or another shell copy/rename operation to turn the draft into the final artifact. Every existing stage gets `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`. Every existing task gets a full detail block, exact `**Task envelope**` marker followed by its Required/Run Only Once/Skip Condition table, and the matching type-specific detail block. Use concise default detail tables when the draft has only task summaries, but preserve exact stage and task display names (including punctuation), task types, variables, conditions, connector placeholders, and domain rules; structural normalization never renames business elements. A thresholded actor in draft prose/personas must also become executable inside an existing task — use a guarded owner/recipient/assignment expression that names the threshold and actor on the same line (for example, `=js:vars.loanAmount > 5000000 ? "Role:CreditAnalyst" : "Role:Underwriter"`); persona prose alone is not final, and this normalization must not add or rename a task. Secondary-stage task headings must be normalized to `##### Task S{secondaryStageIndex}.{taskIndex}: {Task Name}`; never preserve draft letter prefixes like `R.1`, `W.1`, `CC.1`, or `ESC.1`. For a large draft that needs batched writes, first Write the complete ordered document skeleton — Sections 1–4 and every primary/secondary stage heading in source order inside Section 2 — then Edit each stage/task block in place. Never append a deferred or omitted stage after `## Section 3`; insert it at its existing Section 2 heading before continuing. Before writing, confirm the output has the same ordered stage/task inventory and that every stage/task block carries its required literal markers: stage `**Design Rationale:**`, `#### Stage Entry Conditions`, `#### Stage Exit Conditions`, and `#### Tasks`; task `**Activation Mode:**`, `**Design Rationale:**`, `**Task envelope**`, and the matching type-specific detail heading. Section 2 is incomplete until every inventoried stage and task appears before `## Section 3`. Then write `sdd.md` with Write/Edit and stop. ## What to say while working diff --git a/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py b/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py index 4fa9181c8c..902aef797a 100644 --- a/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py +++ b/tests/tasks/uipath-maestro-case/athena_cm_event/check_athena_cm_event_plan.py @@ -51,7 +51,8 @@ def task_section(plan: str, task_name: str) -> str: def field(section: str, name: str, task_name: str) -> str: match = re.search( - rf"(?im)^-\s*(?:\*\*)?{re.escape(name)}:(?:\*\*)?\s*`?([a-z][a-z0-9-]*)`?\s*$", + rf"(?im)^-\s*(?:\*\*)?{re.escape(name)}:(?:\*\*)?\s*" + rf"`?([a-z][a-z0-9-]*)(?:\([^\n)]*\))?`?\s*$", section, ) if not match: diff --git a/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py b/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py index ec5c36bcb0..364eb322ce 100644 --- a/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py +++ b/tests/tasks/uipath-maestro-case/athena_cm_event/test_checkers.py @@ -223,11 +223,11 @@ def write_tasks_md(self, *, rewrite_a2_as_sequential: bool = False) -> None: } sections = [] for index, (task_name, (mode, rule, selected)) in enumerate(contracts.items(), 1): - selected_line = f"\n- selected-tasks-ids: {selected}" if selected else "" + rendered_rule = f'{rule}("{selected}")' if selected else rule sections.append( f'## T{index}: Add process task "{task_name}" to "Stage"\n\n' f"- activation-mode: {mode}\n" - f"- entry-rule: {rule}{selected_line}\n" + f"- entry-rule: {rendered_rule}\n" ) tasks = self.workdir / "tasks" / "tasks.md" tasks.parent.mkdir() diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py index 6c32f5d7d7..c45708e887 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/check_picker_pairing.py @@ -63,14 +63,21 @@ def main() -> None: block = find_stage(blocks, label) if stage_kind(block) != "primary": fail(f"{label!r} must remain a primary stage") - valid_rows = [ + completion_rows = [ row for row in exit_rows(block) if rule_type(row) == "required-tasks-completed" - and "wait-for-user" in column(row, "exit type").lower() - and column(row, "marks stage complete").lower().startswith("y") ] - if not valid_rows: + if len(completion_rows) != 1: + fail( + f"{label!r} must have exactly one required-tasks-completed completion exit; " + "replace the existing row instead of adding a duplicate" + ) + row = completion_rows[0] + if ( + "wait-for-user" not in column(row, "exit type").lower() + or not column(row, "marks stage complete").lower().startswith("y") + ): fail( f"{label!r} must expose {LANE!r} with a required-tasks-completed / " "wait-for-user / Marks Stage Complete: Yes exit" diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml index b3ba92db6a..e0d1a2af23 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/finalize_picker_pairing.yaml @@ -37,7 +37,9 @@ initial_prompt: | Repair mechanically required rule pairings without changing business intent: a manual stage-picker lane needs `wait-for-user` exposure from every primary stage where it may be selected. That exposure is not automatic event, SLA, - or decision routing. + or decision routing. Replace each primary stage's existing completing + `required-tasks-completed` row with one `wait-for-user` / Marks Complete Yes + row; do not add a second, non-completing row. success_criteria: - type: file_exists diff --git a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py index 9ead2a0bd1..3a9beb0741 100644 --- a/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py +++ b/tests/tasks/uipath-maestro-case/phase_0_finalize_draft_picker/test_check_picker_pairing.py @@ -38,11 +38,17 @@ def write_sdd( exposed_by: tuple[str, ...], *, noncompleting: tuple[str, ...] = (), + duplicate_completion: tuple[str, ...] = (), ) -> None: primary_blocks = [] for index, stage_name in enumerate(PRIMARY_STAGES, 1): exit_type = "wait-for-user" if stage_name in exposed_by else "exit-only" marks_complete = "No" if stage_name in noncompleting else "Yes" + duplicate_row = ( + "\n| required-tasks-completed | — | exit-only | Yes |" + if stage_name in duplicate_completion + else "" + ) primary_blocks.append( f"""### Stage {index}: {stage_name} @@ -50,7 +56,7 @@ def write_sdd( | WHEN | IF | Exit Type | Marks Stage Complete | |---|---|---|---| -| required-tasks-completed | — | {exit_type} | {marks_complete} | +| required-tasks-completed | — | {exit_type} | {marks_complete} |{duplicate_row} """ ) text = "# SDD — VendorOnboarding\n\n" + "\n".join(primary_blocks) + """ @@ -87,6 +93,12 @@ def test_rejects_noncompleting_wait_for_user_exit(self) -> None: self.assertNotEqual(result.returncode, 0) self.assertIn("Vendor Approval", result.stdout + result.stderr) + def test_rejects_duplicate_completion_exit_instead_of_replacement(self) -> None: + self.write_sdd(PRIMARY_STAGES, duplicate_completion=("Document Collection",)) + result = run(self.workdir) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Document Collection", result.stdout + result.stderr) + if __name__ == "__main__": unittest.main() From 875b961d817a851082088bb9eda3f73523ab5077 Mon Sep 17 00:00:00 2001 From: abhiram-vad Date: Tue, 4 Aug 2026 09:38:51 -0700 Subject: [PATCH 4/5] fix(catalog): group process mining skill --- skills.sh.json | 1 + 1 file changed, 1 insertion(+) diff --git a/skills.sh.json b/skills.sh.json index 0820e02d4e..eaa95777f5 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -38,6 +38,7 @@ "uipath-test", "uipath-governance", "uipath-insights", + "uipath-process-mining", "uipath-tasks", "uipath-mcp-servers" ] From a38b5a83690b8a1b592fc649dcf6fdefbec8e51c Mon Sep 17 00:00:00 2001 From: abhiram-vad Date: Tue, 4 Aug 2026 12:06:11 -0700 Subject: [PATCH 5/5] chore: remove unrelated catalog change --- skills.sh.json | 1 - 1 file changed, 1 deletion(-) diff --git a/skills.sh.json b/skills.sh.json index eaa95777f5..0820e02d4e 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -38,7 +38,6 @@ "uipath-test", "uipath-governance", "uipath-insights", - "uipath-process-mining", "uipath-tasks", "uipath-mcp-servers" ]