@emiliosp/pi-maestro 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/ADR.md +82 -23
  2. package/CHANGELOG.md +48 -28
  3. package/MISSION.md +2 -4
  4. package/README.md +55 -25
  5. package/TECH_STACK.md +3 -3
  6. package/agents/builder.md +53 -57
  7. package/agents/simplifier.md +79 -61
  8. package/agents/verifier.md +52 -64
  9. package/docs/configuration.md +1 -1
  10. package/docs/glossary.md +43 -40
  11. package/docs/subagent-integration.md +7 -8
  12. package/docs/workflow.md +137 -63
  13. package/extensions/maestro-subagent.ts +1 -0
  14. package/extensions/maestro.ts +60 -30
  15. package/package.json +38 -40
  16. package/src/MaestroPaths.ts +22 -35
  17. package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +6 -15
  18. package/src/artifacts/builder-handoff/schema.ts +3 -6
  19. package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +3 -3
  20. package/src/artifacts/simplifier-handoff/schema.ts +1 -0
  21. package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +4 -10
  22. package/src/artifacts/verifier-handoff/schema.ts +3 -6
  23. package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +1 -3
  24. package/src/config/assertConfiguration.ts +3 -6
  25. package/src/config/defaults.ts +1 -5
  26. package/src/config/loadConfiguration.ts +4 -11
  27. package/src/config/schema.ts +1 -2
  28. package/src/ids/createSpecId.ts +1 -2
  29. package/src/ids/isValidSpecId.ts +1 -2
  30. package/src/maestro/checks/assertEnvironment.ts +19 -50
  31. package/src/maestro/instructions/getMaestroInstructions.ts +47 -26
  32. package/src/maestro/session/MaestroSessionState.ts +15 -0
  33. package/src/maestro/status/formatMaestroStatus.ts +4 -13
  34. package/src/maestro/status/refreshMaestroStatus.ts +3 -7
  35. package/src/specs/create.ts +14 -4
  36. package/src/specs/template.ts +10 -14
  37. package/src/tools/child/record-builder-handoff.ts +1 -0
  38. package/src/tools/child/record-simplifier-handoff.ts +4 -4
  39. package/src/tools/child/record-verifier-handoff.ts +2 -3
  40. package/src/tools/child/utils/resolveWorkflowContext.ts +1 -4
  41. package/src/tools/main/create-spec.ts +3 -2
  42. package/src/tools/main/mark-spec-ready.ts +2 -1
  43. package/src/tools/main/resolve-escalations.ts +1 -0
  44. package/src/tools/main/resolve-findings.ts +6 -20
  45. package/src/tools/main/run-builder.ts +37 -57
  46. package/src/tools/main/run-simplifier.ts +10 -20
  47. package/src/tools/main/run-verifier.ts +31 -55
  48. package/src/tools/utils/pi-subagent-delegation.ts +13 -22
  49. package/src/tools/utils/resolveToolRunContext.ts +2 -3
  50. package/src/utils/is-errno-exception.ts +1 -3
  51. package/src/utils/path-exists.ts +1 -0
  52. package/src/utils/path-strictly-within.ts +3 -6
  53. package/src/utils/path-within-or-equal.ts +1 -4
  54. package/src/utils/read-json.ts +1 -4
  55. package/src/utils/write-json.ts +1 -4
  56. package/src/workflow/builder/completeBuilderPass.ts +3 -2
  57. package/src/workflow/builder/prepareBuilderRun.ts +3 -8
  58. package/src/workflow/builder/resolveEscalations.ts +2 -6
  59. package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -3
  60. package/src/workflow/simplifier/completeSimplifierPass.ts +2 -3
  61. package/src/workflow/spec/markSpecReady.ts +10 -4
  62. package/src/workflow/state/readWorkflowState.ts +2 -7
  63. package/src/workflow/state/schema.ts +5 -10
  64. package/src/workflow/state/writeWorkflowState.ts +1 -4
  65. package/src/workflow/transitions.ts +3 -11
  66. package/src/workflow/utils/assertSpecAcceptanceCriteria.ts +2 -1
  67. package/src/workflow/utils/getAgentRole.ts +1 -3
  68. package/src/workflow/verifier/completeVerifierPass.ts +1 -0
  69. package/src/workflow/verifier/resolveFindings.ts +9 -29
  70. package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +5 -20
  71. package/templates/design.md +20 -0
  72. package/templates/proposal.md +23 -0
  73. package/templates/spec.md +21 -50
  74. package/ROADMAP.md +0 -319
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { readFile } from 'node:fs/promises';
7
+
7
8
  import type { MaestroPaths } from '#MaestroPaths.ts';
8
9
  import { pathExists } from '#utils/path-exists.ts';
9
10
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -12,7 +13,8 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
12
13
  import { transitionWorkflow } from '#workflow/transitions.ts';
13
14
 
14
15
  function assertAcceptanceCriterionOrdering(markdown: string): void {
15
- const criteria = [...markdown.matchAll(/^### AC(\d+):/gm)];
16
+ // Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
17
+ const criteria = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)];
16
18
 
17
19
  if (criteria.some((match, index) => Number(match[1]) !== index + 1)) {
18
20
  throw new Error('Fix acceptance criterion ordering in spec.md.');
@@ -46,13 +48,17 @@ export const markSpecReady = async ({
46
48
  throw new Error(`Spec file is missing: ${specFilePath}.`);
47
49
  }
48
50
 
51
+ const proposalFilePath = paths.getProposalFilePath(specId);
52
+
53
+ if (!(await pathExists(proposalFilePath))) {
54
+ throw new Error(`Proposal file is missing: ${proposalFilePath}.`);
55
+ }
56
+
49
57
  const workflowPath = paths.getWorkflowPath(specId);
50
58
  const current = await readWorkflowState(workflowPath);
51
59
 
52
60
  if (current.specId !== specId) {
53
- throw new Error(
54
- `Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`,
55
- );
61
+ throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`);
56
62
  }
57
63
 
58
64
  const state = transitionWorkflow({
@@ -4,14 +4,9 @@
4
4
  */
5
5
 
6
6
  import { readJsonFile } from '#utils/read-json.ts';
7
- import {
8
- assertWorkflowState,
9
- type WorkflowState,
10
- } from '#workflow/state/schema.ts';
7
+ import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
11
8
 
12
- export const readWorkflowState = async (
13
- path: string,
14
- ): Promise<WorkflowState> => {
9
+ export const readWorkflowState = async (path: string): Promise<WorkflowState> => {
15
10
  const state = await readJsonFile({ path, description: 'Workflow state' });
16
11
  assertWorkflowState(state);
17
12
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { type Static, Type } from 'typebox';
7
7
  import { Value } from 'typebox/value';
8
+
8
9
  import { isValidSpecId, SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
9
10
 
10
11
  export const WORKFLOW_STATE_VERSION = '1.0.0';
@@ -58,17 +59,13 @@ export const WorkflowStateSchema = Type.Object(
58
59
  { additionalProperties: false },
59
60
  );
60
61
 
61
- export type WorkflowPhase =
62
- (typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
62
+ export type WorkflowPhase = (typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
63
63
 
64
- export type WorkflowEvent =
65
- (typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
64
+ export type WorkflowEvent = (typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
66
65
 
67
66
  export type WorkflowState = Static<typeof WorkflowStateSchema>;
68
67
 
69
- function assertWorkflowStateSchema(
70
- input: unknown,
71
- ): asserts input is WorkflowState {
68
+ function assertWorkflowStateSchema(input: unknown): asserts input is WorkflowState {
72
69
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
73
70
  throw new Error('Workflow state must be a JSON object.');
74
71
  }
@@ -80,9 +77,7 @@ function assertWorkflowStateSchema(
80
77
  }
81
78
  }
82
79
 
83
- export function assertWorkflowState(
84
- input: unknown,
85
- ): asserts input is WorkflowState {
80
+ export function assertWorkflowState(input: unknown): asserts input is WorkflowState {
86
81
  assertWorkflowStateSchema(input);
87
82
 
88
83
  if (!isValidSpecId(input.specId)) {
@@ -4,10 +4,7 @@
4
4
  */
5
5
 
6
6
  import { writeJson } from '#utils/write-json.ts';
7
- import {
8
- assertWorkflowState,
9
- type WorkflowState,
10
- } from '#workflow/state/schema.ts';
7
+ import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
11
8
 
12
9
  type WriteWorkflowStateInput = {
13
10
  path: string;
@@ -35,10 +35,7 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
35
35
  to: WORKFLOW_PHASES.READY_FOR_BUILDER,
36
36
  },
37
37
  [WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION]: {
38
- from: [
39
- WORKFLOW_PHASES.ESCALATION_DECISIONS,
40
- WORKFLOW_PHASES.FINDING_DECISIONS,
41
- ],
38
+ from: [WORKFLOW_PHASES.ESCALATION_DECISIONS, WORKFLOW_PHASES.FINDING_DECISIONS],
42
39
  to: WORKFLOW_PHASES.DRAFTING_SPEC,
43
40
  },
44
41
  [WORKFLOW_EVENTS.BUILDER_DONE]: {
@@ -80,10 +77,7 @@ type TransitionWorkflowInput = {
80
77
  event: WorkflowEvent;
81
78
  };
82
79
 
83
- export const transitionWorkflow = ({
84
- state,
85
- event,
86
- }: TransitionWorkflowInput): WorkflowState => {
80
+ export const transitionWorkflow = ({ state, event }: TransitionWorkflowInput): WorkflowState => {
87
81
  assertWorkflowState(state);
88
82
  const transition = WORKFLOW_TRANSITIONS[event];
89
83
 
@@ -92,9 +86,7 @@ export const transitionWorkflow = ({
92
86
  }
93
87
 
94
88
  if (!transition.from.includes(state.phase)) {
95
- throw new Error(
96
- `Workflow event "${event}" is not allowed from phase "${state.phase}".`,
97
- );
89
+ throw new Error(`Workflow event "${event}" is not allowed from phase "${state.phase}".`);
98
90
  }
99
91
 
100
92
  return {
@@ -12,7 +12,8 @@ export function assertSpecAcceptanceCriteria({
12
12
  markdown,
13
13
  criterionIds,
14
14
  }: AssertSpecAcceptanceCriteriaInput): void {
15
- const missingIds = [...markdown.matchAll(/^### AC(\d+):/gm)]
15
+ // Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
16
+ const missingIds = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)]
16
17
  .map((match) => `AC${match[1]}`)
17
18
  .filter((id) => !criterionIds.has(id));
18
19
 
@@ -18,9 +18,7 @@ const VERIFIER_PHASES: readonly WorkflowState['phase'][] = [
18
18
  WORKFLOW_PHASES.FINDING_DECISIONS,
19
19
  ];
20
20
 
21
- export const getAgentRole = (
22
- phase: WorkflowState['phase'],
23
- ): WorkflowRole | null => {
21
+ export const getAgentRole = (phase: WorkflowState['phase']): WorkflowRole | null => {
24
22
  if (BUILDER_PHASES.includes(phase)) {
25
23
  return WORKFLOW_ROLES.BUILDER;
26
24
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { mkdir, readFile } from 'node:fs/promises';
7
+
7
8
  import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
8
9
  import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
9
10
  import { writeVerifierHandoff } from '#artifacts/verifier-handoff/writeVerifierHandoff.ts';
@@ -5,10 +5,7 @@
5
5
 
6
6
  import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
7
7
  import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
8
- import type {
9
- VerifierFinding,
10
- VerifierHandoff,
11
- } from '#artifacts/verifier-handoff/schema.ts';
8
+ import type { VerifierFinding, VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
12
9
  import { FINDING_DECISIONS } from '#artifacts/verifier-handoff/schema.ts';
13
10
  import type { MaestroPaths } from '#MaestroPaths.ts';
14
11
  import { writeJson } from '#utils/write-json.ts';
@@ -48,20 +45,13 @@ const applyFindingDecisions = ({
48
45
  }
49
46
 
50
47
  if (decisionsById.has(decision.findingId)) {
51
- throw new Error(
52
- `Duplicate decision for finding "${decision.findingId}".`,
53
- );
48
+ throw new Error(`Duplicate decision for finding "${decision.findingId}".`);
54
49
  }
55
50
 
56
51
  decisionsById.set(decision.findingId, decision);
57
52
 
58
- if (
59
- decision.decision === FINDING_DECISIONS.REJECT &&
60
- decision.reason.trim().length === 0
61
- ) {
62
- throw new Error(
63
- `Rejection reason for "${decision.findingId}" must not be empty.`,
64
- );
53
+ if (decision.decision === FINDING_DECISIONS.REJECT && decision.reason.trim().length === 0) {
54
+ throw new Error(`Rejection reason for "${decision.findingId}" must not be empty.`);
65
55
  }
66
56
  }
67
57
 
@@ -94,15 +84,11 @@ const readFindingWorkflow = async ({
94
84
  const state = await readWorkflowState(paths.getWorkflowPath(specId));
95
85
 
96
86
  if (state.specId !== specId) {
97
- throw new Error(
98
- `Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
99
- );
87
+ throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
100
88
  }
101
89
 
102
90
  if (state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS) {
103
- throw new Error(
104
- `Finding resolution requires finding-decisions state, found "${state.phase}".`,
105
- );
91
+ throw new Error(`Finding resolution requires finding-decisions state, found "${state.phase}".`);
106
92
  }
107
93
 
108
94
  const handoff = await readVerifierHandoff({
@@ -111,23 +97,17 @@ const readFindingWorkflow = async ({
111
97
  });
112
98
 
113
99
  if (handoff.findings.length === 0) {
114
- throw new Error(
115
- 'Finding resolution requires at least one current finding.',
116
- );
100
+ throw new Error('Finding resolution requires at least one current finding.');
117
101
  }
118
102
 
119
103
  if (handoff.findings.some(({ decision }) => decision !== null)) {
120
- throw new Error(
121
- 'Current verifier findings already contain an owner decision.',
122
- );
104
+ throw new Error('Current verifier findings already contain an owner decision.');
123
105
  }
124
106
 
125
107
  return { state, handoff };
126
108
  };
127
109
 
128
- const getSavedFindingDecision = (
129
- decision: FindingDecision | undefined,
130
- ): SavedFindingDecision => {
110
+ const getSavedFindingDecision = (decision: FindingDecision | undefined): SavedFindingDecision => {
131
111
  if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
132
112
  return {
133
113
  decision: FINDING_DECISIONS.REVISE_SPEC,
@@ -3,15 +3,10 @@
3
3
  * Used: When a verifier completes a pass or the owner resolves its findings.
4
4
  */
5
5
 
6
- import {
7
- FINDING_DECISIONS,
8
- type VerifierHandoff,
9
- } from '#artifacts/verifier-handoff/schema.ts';
6
+ import { FINDING_DECISIONS, type VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
10
7
  import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
11
8
 
12
- export const getWorkflowEventByVerifierHandoff = (
13
- handoff: VerifierHandoff,
14
- ): WorkflowEvent => {
9
+ export const getWorkflowEventByVerifierHandoff = (handoff: VerifierHandoff): WorkflowEvent => {
15
10
  if (handoff.findings.length === 0) {
16
11
  return WORKFLOW_EVENTS.VERIFIER_DONE;
17
12
  }
@@ -21,26 +16,16 @@ export const getWorkflowEventByVerifierHandoff = (
21
16
  }
22
17
 
23
18
  if (
24
- handoff.findings.some(
25
- ({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
26
- )
19
+ handoff.findings.some(({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC)
27
20
  ) {
28
21
  return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
29
22
  }
30
23
 
31
- if (
32
- handoff.findings.some(
33
- ({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
34
- )
35
- ) {
24
+ if (handoff.findings.some(({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE)) {
36
25
  return WORKFLOW_EVENTS.OWNER_REQUEST_FIXES;
37
26
  }
38
27
 
39
- if (
40
- handoff.findings.every(
41
- ({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
42
- )
43
- ) {
28
+ if (handoff.findings.every(({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT)) {
44
29
  return WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS;
45
30
  }
46
31
 
@@ -0,0 +1,20 @@
1
+ # <id>: <short, outcome-oriented title>
2
+
3
+ <!--
4
+ Create this document only when significant technical decisions must be preserved.
5
+ Record approved architectural decisions, important technical constraints, or prototype choices, not routine implementation details.
6
+ Keep motivation and scope in proposal.md. Keep required behavior, edge cases, and acceptance criteria in spec.md.
7
+ Use only the sections that matter. Omit empty sections and these instructions.
8
+ -->
9
+
10
+ ## Decisions
11
+
12
+ <Describe each approved technical decision and its brief reason. Include only alternatives actually considered and consequences that matter. Link related REQ IDs in spec.md instead of repeating requirements.>
13
+
14
+ ## Contracts and prototypes
15
+
16
+ <Include interface or data contracts and links to prototypes only when needed to explain the approved design. Keep observable behavior in spec.md and distinguish illustrative prototype content from requirements.>
17
+
18
+ ## Risks and limits
19
+
20
+ <Record relevant technical risks, migration or rollback constraints, and evidence supporting the decision. Do not add investigation logs. Resolve decisions affecting the contract with the owner before approval.>
@@ -0,0 +1,23 @@
1
+ # <id>: <short, outcome-oriented title>
2
+
3
+ <!--
4
+ Explain why this change exists and where its scope ends. Keep it brief and write for the owner.
5
+ Keep detailed behavior and acceptance criteria in spec.md, and approved technical decisions in design.md when needed.
6
+ Omit empty sections and these instructions. Do not duplicate requirements or investigation logs.
7
+ -->
8
+
9
+ ## Problem
10
+
11
+ <Describe the current problem, who is affected, and why the change is needed.>
12
+
13
+ ## Goal and scope
14
+
15
+ <State the intended outcome and the boundaries of this one reversible change.>
16
+
17
+ ## Out of scope
18
+
19
+ <Name related changes that are explicitly excluded.>
20
+
21
+ ## Required behavior
22
+
23
+ See [spec.md](spec.md).
package/templates/spec.md CHANGED
@@ -1,69 +1,40 @@
1
1
  # <id>: <short, outcome-oriented title>
2
2
 
3
- > After owner approval, this specification is the contract for the builder and verifier.
4
-
5
3
  <!--
6
- Write for the owner, not only for agents. Keep detail proportional to the change.
7
- State each requirement once. Refer to it from acceptance criteria instead of repeating it.
8
- Use the topics below as guidance, not a checklist to fill. Omit empty subsections and these instructions.
9
- Leave local implementation choices, test code, fixtures, mocks, and commands to the builder.
4
+ Define required behavior, not motivation or implementation. See proposal.md for scope and design.md, when present, for approved technical decisions.
5
+ Write for the owner. State each requirement once and keep its acceptance criteria immediately below it.
6
+ Use the topics below as guidance. Omit empty subsections and these instructions.
7
+ Leave test code, fixtures, mocks, and commands to the builder. Keep execution evidence in handoffs.
10
8
  -->
11
9
 
12
- ## 1. Context, goals, and scope
13
-
14
- <Briefly describe the problem, who is affected, and the intended outcome.>
15
-
16
- ### Out of scope
17
-
18
- <Name related changes that are explicitly excluded.>
19
-
20
- ## 2. Requirements and constraints
10
+ ## Requirements
21
11
 
22
- <Describe required behavior and measurable limits. Include performance, reliability, accessibility, or other quality requirements only when relevant.>
12
+ <Give each requirement its own ### REQ<number>: <title> heading and a unique, stable ID. Document only behavior or constraints this change adds, modifies, or removes, with relevant measurable limits, edge cases, and error handling. Existing behavior must continue working unless explicitly changed or removed; do not list unchanged behavior as requirements. Do not copy repository or agent instructions.>
23
13
 
24
- <Include non-negotiable boundaries and existing behavior that this change must preserve. Do not copy repository or agent instructions.>
14
+ <Give each requirement exactly one Delta value: added, modified, or removed. Keep requirements organized by topic, not by delta category. For modified or removed requirements, add a brief Before description only when useful. Verify the previous behavior from repository evidence; do not invent it or repeat the whole old requirement. State the complete required outcome, including the required absence and any agreed replacement or error behavior for removals. Keep the same pre-change baseline during revisions.>
25
15
 
26
- ### Edge cases and error handling
16
+ <Each requirement must have Verified by with Markdown links to its acceptance criteria, using their actual heading anchors. When an existing test covers the exact starting conditions, action, and expected result, link its file relative to spec.md and give the exact scenario name instead of creating a duplicate criterion. Keep criteria for partial or uncertain coverage. A requirement can reference both criteria and tests; check their combined coverage.>
27
17
 
28
- <Describe boundary conditions, failures, and required recovery behavior not already covered above. Keep these requirements even when existing tests cover their scenarios.>
18
+ <Write each requirement-associated AC once, immediately below its primary requirement, with a #### AC<number>: <title> heading. Other requirements can link to it without duplication. A relevant regression AC without an associated changed requirement can instead use a ### AC<number>: <title> heading under an optional ## Regression checks section, with the same probe, expected result, and example fields. Do not invent a REQ or Delta field for it. Include only regressions relevant to this change, not an inventory of existing behavior, and omit the section when unnecessary.>
29
19
 
30
- ## 3. Technical decisions and prototypes
20
+ <Number ACs continuously from AC1 across the whole document, including nested and standalone regression criteria. During review, remove criteria already covered exactly by existing tests and update numbering and affected links. Keep required behavior documented when a requirement-associated criterion is omitted; omitting a standalone regression criterion does not require creating a requirement.>
31
21
 
32
- <Record only approved architectural decisions or technical constraints that affect scope or behavior. Explain their reasons briefly. Leave routine implementation choices to the builder.>
22
+ ### REQ1: <short required behavior>
33
23
 
34
- <For each relevant topic below, add a short subsection. Omit topics that do not apply.>
24
+ Delta: <added | modified | removed>
35
25
 
36
- 1. Components and data flow: changed responsibilities, data contracts, and important boundaries. No file inventory or function-level design.
37
- 2. API specification: changed operations, permissions, inputs, outputs, errors, side effects, and delivery guarantees.
38
- 3. Prototype and user interaction: link prototypes and identify approved states and interactions. Distinguish illustrative content from requirements.
39
- 4. External integrations: affected services, failure behavior, and retries.
40
- 5. Security and privacy: access rules, sensitive data, retention, and trust boundaries.
41
- 6. Compatibility and migration: compatibility limits, migration, rollout, and rollback requirements.
42
- 7. Monitoring and observability: required signals, alerts, and ownership.
26
+ <Describe the requirement once, including relevant boundary conditions and required failure or recovery behavior.>
43
27
 
44
- <Include only evidence and unresolved limits that affect an owner decision. Do not include investigation logs or general verification disclaimers.>
28
+ Verified by: [AC1](#ac1-short-observable-claim)
45
29
 
46
- ## 4. Acceptance criteria
30
+ #### AC1: <short observable claim>
47
31
 
48
- <Inspect existing tests before writing criteria. Omit a criterion only if a test covers the same starting conditions, action, and expected result. Keep criteria when coverage is partial or uncertain. During review, remove criteria already covered by tests. Keep numbering continuous from AC1 and update affected references. Keep required behavior in section 2 even when its criterion is omitted.>
49
-
50
- <Give each criterion a unique ID, one observable claim, and one coverage label. A probe is the scenario used to check that claim. Describe it in plain language. Use success for a valid action that produces the required result. Use failure for an invalid action that is correctly rejected. Use unchanged when the criterion explicitly checks existing behavior that the change must preserve. The label describes the scenario, not the test outcome. A failure scenario passes when the invalid action is correctly rejected. Do not require all three categories.>
51
-
52
- ### AC1: <short observable claim>
53
-
54
- 1. Coverage: <success | failure | unchanged>
55
- 2. Probe: <starting conditions and action or observation, without prescribing test implementation>
56
- 3. Expected result: <observable and measurable outcome>
57
- 4. Example: <specific starting conditions, concrete input or action, and exact expected result for this probe, not a restatement of the claim>
32
+ 1. Probe: <starting conditions and action or observation, without prescribing test implementation>
33
+ 2. Expected result: <observable and measurable outcome>
34
+ 3. Example: <specific starting conditions, concrete input or action, and exact expected result, not a restatement of the claim>
58
35
 
59
36
  <!--
60
- Example:
61
- Coverage: success
62
- Probe: Save a change to an item's title, then reopen the item.
63
- Expected result: The saved title is still present.
64
- Example: An item's title is "Draft". Change it to "Ready", save, and reopen the item. The title is "Ready".
65
-
66
- The builder and verifier each run every probe.
67
- The builder chooses executable checks. The verifier checks their coverage independently.
68
- Keep execution evidence in handoffs, not in this spec.
37
+ Each AC has one observable claim, a probe, an expected result, and a concrete example.
38
+ Describe relevant valid actions, invalid actions, and regressions directly through those fields, without a separate classification label.
39
+ The builder and verifier each run every probe independently. The builder chooses executable checks; the verifier checks their coverage independently.
69
40
  -->