@nanobpm/nano-workforce 0.104.0 → 0.106.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 (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/app/abandon.ts +12 -3
  4. package/app/agentCompletion.test.ts +28 -0
  5. package/app/agentCompletion.ts +14 -2
  6. package/app/conformance.test.ts +313 -0
  7. package/app/conformance.ts +329 -0
  8. package/app/dbFence.ts +18 -0
  9. package/app/instance-tracking.test.ts +24 -0
  10. package/app/migration053.test.ts +84 -0
  11. package/app/plan.ts +6 -0
  12. package/app/pollUserTasks.test.ts +37 -0
  13. package/app/retro.test.ts +32 -2
  14. package/app/retro.ts +26 -10
  15. package/app/service.test.ts +191 -3
  16. package/app/service.ts +163 -2
  17. package/app/userTasks.test.ts +20 -0
  18. package/app/userTasks.ts +2 -0
  19. package/app/waves.test.ts +12 -0
  20. package/app/world/store.ts +6 -6
  21. package/db/migrations/004_planning.sql +1 -1
  22. package/db/migrations/052_plan_conformance.sql +28 -0
  23. package/db/migrations/053_merges_abandon_dedupe.sql +29 -0
  24. package/db/migrations/054_conformance_review_tracking.sql +27 -0
  25. package/nano.app.json +24 -1
  26. package/package.json +1 -1
  27. package/pages/tasks.page.json +84 -0
  28. package/resources/forms/conformance-escalation.form +17 -0
  29. package/resources/processes/retro.bpmn +123 -11
  30. package/resources/prompts/conformance.md +105 -0
  31. package/resources/prompts/retro.md +5 -0
  32. package/workers/conformance-ack/worker.test.ts +50 -0
  33. package/workers/conformance-ack/worker.ts +31 -0
  34. package/workers/conformance-record/worker.test.ts +241 -0
  35. package/workers/conformance-record/worker.ts +127 -0
  36. package/workers/merge/worker.test.ts +4 -0
  37. package/workers/merge/worker.ts +13 -18
  38. package/workers/retro-gather/worker.test.ts +6 -0
  39. package/workers/retro-gather/worker.ts +17 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.104.0",
3
+ "version": "0.106.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -532,6 +532,90 @@
532
532
  },
533
533
  "refreshMs": 5000
534
534
  }
535
+ },
536
+ {
537
+ "type": "dataGrid",
538
+ "id": "conformance-reviews",
539
+ "props": {
540
+ "title": "Conformance reviews",
541
+ "data": {
542
+ "kind": "datasource",
543
+ "source": "app",
544
+ "table": "user_tasks",
545
+ "filter": [
546
+ {
547
+ "field": "element_id",
548
+ "in": [
549
+ "conformance-escalation"
550
+ ]
551
+ }
552
+ ],
553
+ "orderBy": {
554
+ "field": "updated_at",
555
+ "dir": "desc"
556
+ }
557
+ },
558
+ "columns": [
559
+ {
560
+ "field": "subject_title",
561
+ "template": "{{subject_title}}",
562
+ "header": "Epic",
563
+ "subtitleField": "subject_key",
564
+ "truncate": true,
565
+ "linkField": "subject_url"
566
+ },
567
+ {
568
+ "field": "question",
569
+ "header": "Findings"
570
+ },
571
+ {
572
+ "field": "subject_url",
573
+ "header": "Issue"
574
+ },
575
+ {
576
+ "field": "process_key",
577
+ "header": "Process",
578
+ "link": {
579
+ "kind": "processExplorer",
580
+ "keyField": "process_key"
581
+ }
582
+ },
583
+ {
584
+ "field": "updated_at",
585
+ "header": "Updated",
586
+ "format": "datetime"
587
+ }
588
+ ],
589
+ "rowKey": "user_task_key",
590
+ "detail": {
591
+ "linkField": "subject_url",
592
+ "fields": [
593
+ {
594
+ "field": "question",
595
+ "label": "Findings"
596
+ }
597
+ ],
598
+ "form": {
599
+ "showWhenField": "user_task_key",
600
+ "title": "Acknowledge conformance review",
601
+ "promptField": "question",
602
+ "inputKey": "note",
603
+ "inputLabel": "Disposition note (what you decided / follow-up)",
604
+ "submitLabel": "Acknowledge & close",
605
+ "action": {
606
+ "path": "/app/api/actions/complete-user-task",
607
+ "successLabel": "Acknowledged — the review will close",
608
+ "body": {
609
+ "userTaskKey": "{{row.user_task_key}}",
610
+ "variables": {
611
+ "note": "{{form.note}}"
612
+ }
613
+ }
614
+ }
615
+ }
616
+ },
617
+ "refreshMs": 5000
618
+ }
535
619
  }
536
620
  ]
537
621
  }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "conformance-escalation",
3
+ "schemaVersion": 18,
4
+ "type": "default",
5
+ "components": [
6
+ {
7
+ "type": "text",
8
+ "text": "The spec-conformance review found this epic did **not** cleanly meet its spec — a slice was reduced or could not be verified against the code, or a deviation was made that nobody raised during implementation. The delivery already landed, so this is a **non-blocking** follow-up: review the report on the epic issue, decide any remediation, then acknowledge to close it.",
9
+ "label": "Conformance review"
10
+ },
11
+ {
12
+ "type": "textarea",
13
+ "key": "note",
14
+ "label": "Disposition note (what you decided / follow-up — recorded on the review)"
15
+ }
16
+ ]
17
+ }
@@ -13,6 +13,18 @@
13
13
  <nano:extend name="pr" type="string" optional="true" />
14
14
  <nano:extend name="summary" type="string" optional="true" />
15
15
  </nano:shape>
16
+ <nano:shape id="ConformanceRecordIn" name="Record conformance — input">
17
+ <nano:extend name="planKey" type="string" />
18
+ <nano:extend name="status" type="string" optional="true" />
19
+ <nano:extend name="commentUrl" type="string" optional="true" />
20
+ <nano:extend name="slicesMet" type="integer" optional="true" />
21
+ <nano:extend name="slicesReduced" type="integer" optional="true" />
22
+ <nano:extend name="slicesNotVerified" type="integer" optional="true" />
23
+ <nano:extend name="deviationsRaised" type="integer" optional="true" />
24
+ <nano:extend name="deviationsUnraised" type="integer" optional="true" />
25
+ <nano:extend name="hasDeviations" type="boolean" optional="true" />
26
+ <nano:extend name="summary" type="string" optional="true" />
27
+ </nano:shape>
16
28
  </nano:shapes>
17
29
  </bpmn:extensionElements>
18
30
  <bpmn:startEvent id="Start" name="Epic complete">
@@ -26,7 +38,55 @@
26
38
  </zeebe:properties>
27
39
  </bpmn:extensionElements>
28
40
  <bpmn:incoming>f_start</bpmn:incoming>
29
- <bpmn:outgoing>f_toSynthesize</bpmn:outgoing>
41
+ <bpmn:outgoing>f_toConformance</bpmn:outgoing>
42
+ </bpmn:serviceTask>
43
+ <bpmn:serviceTask id="conformance" name="Verify implementation vs spec (agent)">
44
+ <bpmn:extensionElements>
45
+ <zeebe:taskDefinition type="senior:conformance" />
46
+ <zeebe:linkedResources>
47
+ <zeebe:linkedResource resourceId="conformance.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
48
+ </zeebe:linkedResources>
49
+ <zeebe:ioMapping>
50
+ <zeebe:input source="=conformanceDigest" target="appendPrompt" />
51
+ </zeebe:ioMapping>
52
+ </bpmn:extensionElements>
53
+ <bpmn:incoming>f_toConformance</bpmn:incoming>
54
+ <bpmn:outgoing>f_toRecordConformance</bpmn:outgoing>
55
+ </bpmn:serviceTask>
56
+ <bpmn:serviceTask id="record-conformance" name="Record conformance">
57
+ <bpmn:extensionElements>
58
+ <zeebe:taskDefinition type="pr.conformance-record" />
59
+ <zeebe:properties>
60
+ <zeebe:property name="io.nanobpm.dataEnvelope.in" value="ConformanceRecordIn" />
61
+ </zeebe:properties>
62
+ </bpmn:extensionElements>
63
+ <bpmn:incoming>f_toRecordConformance</bpmn:incoming>
64
+ <bpmn:outgoing>f_toDeviationsGw</bpmn:outgoing>
65
+ </bpmn:serviceTask>
66
+ <bpmn:exclusiveGateway id="gw-deviations" name="deviations?" default="f_noDeviations">
67
+ <bpmn:incoming>f_toDeviationsGw</bpmn:incoming>
68
+ <bpmn:outgoing>f_deviations</bpmn:outgoing>
69
+ <bpmn:outgoing>f_noDeviations</bpmn:outgoing>
70
+ </bpmn:exclusiveGateway>
71
+ <bpmn:userTask id="conformance-escalation" name="Conformance review (human)">
72
+ <bpmn:extensionElements>
73
+ <zeebe:formDefinition formId="conformance-escalation" />
74
+ <zeebe:userTask />
75
+ <zeebe:assignmentDefinition candidateGroups="operators" />
76
+ </bpmn:extensionElements>
77
+ <bpmn:incoming>f_deviations</bpmn:incoming>
78
+ <bpmn:outgoing>f_toConformanceAck</bpmn:outgoing>
79
+ </bpmn:userTask>
80
+ <bpmn:serviceTask id="record-conformance-ack" name="Record acknowledgement">
81
+ <bpmn:extensionElements>
82
+ <zeebe:taskDefinition type="pr.conformance-ack" />
83
+ <zeebe:ioMapping>
84
+ <zeebe:input source="=planKey" target="planKey" />
85
+ <zeebe:input source="=if (is defined(note)) then note else null" target="note" />
86
+ </zeebe:ioMapping>
87
+ </bpmn:extensionElements>
88
+ <bpmn:incoming>f_toConformanceAck</bpmn:incoming>
89
+ <bpmn:outgoing>f_ackToSynthesize</bpmn:outgoing>
30
90
  </bpmn:serviceTask>
31
91
  <bpmn:serviceTask id="synthesize" name="Synthesize &#38; promote (agent)">
32
92
  <bpmn:extensionElements>
@@ -38,7 +98,8 @@
38
98
  <zeebe:input source="=retroDigest" target="appendPrompt" />
39
99
  </zeebe:ioMapping>
40
100
  </bpmn:extensionElements>
41
- <bpmn:incoming>f_toSynthesize</bpmn:incoming>
101
+ <bpmn:incoming>f_noDeviations</bpmn:incoming>
102
+ <bpmn:incoming>f_ackToSynthesize</bpmn:incoming>
42
103
  <bpmn:outgoing>f_toRecord</bpmn:outgoing>
43
104
  </bpmn:serviceTask>
44
105
  <bpmn:serviceTask id="record" name="Record retro">
@@ -55,7 +116,15 @@
55
116
  <bpmn:incoming>f_toEnd</bpmn:incoming>
56
117
  </bpmn:endEvent>
57
118
  <bpmn:sequenceFlow id="f_start" sourceRef="Start" targetRef="gather" />
58
- <bpmn:sequenceFlow id="f_toSynthesize" sourceRef="gather" targetRef="synthesize" />
119
+ <bpmn:sequenceFlow id="f_toConformance" sourceRef="gather" targetRef="conformance" />
120
+ <bpmn:sequenceFlow id="f_toRecordConformance" sourceRef="conformance" targetRef="record-conformance" />
121
+ <bpmn:sequenceFlow id="f_toDeviationsGw" sourceRef="record-conformance" targetRef="gw-deviations" />
122
+ <bpmn:sequenceFlow id="f_deviations" name="deviations" sourceRef="gw-deviations" targetRef="conformance-escalation">
123
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=hasDeviations = true</bpmn:conditionExpression>
124
+ </bpmn:sequenceFlow>
125
+ <bpmn:sequenceFlow id="f_noDeviations" sourceRef="gw-deviations" targetRef="synthesize" />
126
+ <bpmn:sequenceFlow id="f_toConformanceAck" sourceRef="conformance-escalation" targetRef="record-conformance-ack" />
127
+ <bpmn:sequenceFlow id="f_ackToSynthesize" sourceRef="record-conformance-ack" targetRef="synthesize" />
59
128
  <bpmn:sequenceFlow id="f_toRecord" sourceRef="synthesize" targetRef="record" />
60
129
  <bpmn:sequenceFlow id="f_toEnd" sourceRef="record" targetRef="End" />
61
130
  </bpmn:process>
@@ -70,33 +139,76 @@
70
139
  <bpmndi:BPMNShape id="BPMNShape_gather" bpmnElement="gather">
71
140
  <dc:Bounds x="216" y="80" width="100" height="80" />
72
141
  </bpmndi:BPMNShape>
73
- <bpmndi:BPMNShape id="BPMNShape_synthesize" bpmnElement="synthesize">
142
+ <bpmndi:BPMNShape id="BPMNShape_conformance" bpmnElement="conformance">
74
143
  <dc:Bounds x="416" y="80" width="100" height="80" />
75
144
  </bpmndi:BPMNShape>
76
- <bpmndi:BPMNShape id="BPMNShape_record" bpmnElement="record">
145
+ <bpmndi:BPMNShape id="BPMNShape_record-conformance" bpmnElement="record-conformance">
77
146
  <dc:Bounds x="616" y="80" width="100" height="80" />
78
147
  </bpmndi:BPMNShape>
148
+ <bpmndi:BPMNShape id="BPMNShape_gw-deviations" bpmnElement="gw-deviations" isMarkerVisible="true">
149
+ <dc:Bounds x="776" y="95" width="50" height="50" />
150
+ <bpmndi:BPMNLabel>
151
+ <dc:Bounds x="770" y="65" width="62" height="14" />
152
+ </bpmndi:BPMNLabel>
153
+ </bpmndi:BPMNShape>
154
+ <bpmndi:BPMNShape id="BPMNShape_conformance-escalation" bpmnElement="conformance-escalation">
155
+ <dc:Bounds x="751" y="240" width="100" height="80" />
156
+ </bpmndi:BPMNShape>
157
+ <bpmndi:BPMNShape id="BPMNShape_record-conformance-ack" bpmnElement="record-conformance-ack">
158
+ <dc:Bounds x="911" y="240" width="100" height="80" />
159
+ </bpmndi:BPMNShape>
160
+ <bpmndi:BPMNShape id="BPMNShape_synthesize" bpmnElement="synthesize">
161
+ <dc:Bounds x="1056" y="80" width="100" height="80" />
162
+ </bpmndi:BPMNShape>
163
+ <bpmndi:BPMNShape id="BPMNShape_record" bpmnElement="record">
164
+ <dc:Bounds x="1256" y="80" width="100" height="80" />
165
+ </bpmndi:BPMNShape>
79
166
  <bpmndi:BPMNShape id="BPMNShape_End" bpmnElement="End">
80
- <dc:Bounds x="816" y="102" width="36" height="36" />
167
+ <dc:Bounds x="1456" y="102" width="36" height="36" />
81
168
  <bpmndi:BPMNLabel>
82
- <dc:Bounds x="794" y="143" width="80" height="14" />
169
+ <dc:Bounds x="1434" y="143" width="80" height="14" />
83
170
  </bpmndi:BPMNLabel>
84
171
  </bpmndi:BPMNShape>
85
172
  <bpmndi:BPMNEdge id="BPMNEdge_f_start" bpmnElement="f_start">
86
173
  <di:waypoint x="116" y="120" />
87
174
  <di:waypoint x="216" y="120" />
88
175
  </bpmndi:BPMNEdge>
89
- <bpmndi:BPMNEdge id="BPMNEdge_f_toSynthesize" bpmnElement="f_toSynthesize">
176
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toConformance" bpmnElement="f_toConformance">
90
177
  <di:waypoint x="316" y="120" />
91
178
  <di:waypoint x="416" y="120" />
92
179
  </bpmndi:BPMNEdge>
93
- <bpmndi:BPMNEdge id="BPMNEdge_f_toRecord" bpmnElement="f_toRecord">
180
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toRecordConformance" bpmnElement="f_toRecordConformance">
94
181
  <di:waypoint x="516" y="120" />
95
182
  <di:waypoint x="616" y="120" />
96
183
  </bpmndi:BPMNEdge>
97
- <bpmndi:BPMNEdge id="BPMNEdge_f_toEnd" bpmnElement="f_toEnd">
184
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toDeviationsGw" bpmnElement="f_toDeviationsGw">
98
185
  <di:waypoint x="716" y="120" />
99
- <di:waypoint x="816" y="120" />
186
+ <di:waypoint x="776" y="120" />
187
+ </bpmndi:BPMNEdge>
188
+ <bpmndi:BPMNEdge id="BPMNEdge_f_noDeviations" bpmnElement="f_noDeviations">
189
+ <di:waypoint x="826" y="120" />
190
+ <di:waypoint x="1056" y="120" />
191
+ </bpmndi:BPMNEdge>
192
+ <bpmndi:BPMNEdge id="BPMNEdge_f_deviations" bpmnElement="f_deviations">
193
+ <di:waypoint x="801" y="145" />
194
+ <di:waypoint x="801" y="240" />
195
+ </bpmndi:BPMNEdge>
196
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toConformanceAck" bpmnElement="f_toConformanceAck">
197
+ <di:waypoint x="851" y="280" />
198
+ <di:waypoint x="911" y="280" />
199
+ </bpmndi:BPMNEdge>
200
+ <bpmndi:BPMNEdge id="BPMNEdge_f_ackToSynthesize" bpmnElement="f_ackToSynthesize">
201
+ <di:waypoint x="1011" y="280" />
202
+ <di:waypoint x="1106" y="280" />
203
+ <di:waypoint x="1106" y="160" />
204
+ </bpmndi:BPMNEdge>
205
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toRecord" bpmnElement="f_toRecord">
206
+ <di:waypoint x="1156" y="120" />
207
+ <di:waypoint x="1256" y="120" />
208
+ </bpmndi:BPMNEdge>
209
+ <bpmndi:BPMNEdge id="BPMNEdge_f_toEnd" bpmnElement="f_toEnd">
210
+ <di:waypoint x="1356" y="120" />
211
+ <di:waypoint x="1456" y="120" />
100
212
  </bpmndi:BPMNEdge>
101
213
  </bpmndi:BPMNPlane>
102
214
  </bpmndi:BPMNDiagram>
@@ -0,0 +1,105 @@
1
+ # Conformance agent — verify the implementation against the spec, by reading the code
2
+
3
+ You are the **spec-conformance agent**. An epic (a fan-out `plan`) has finished — every one of its
4
+ slices has reached a terminal state and the landed PRs are now in the tree. Your job is to answer a
5
+ single, evidence-based question for a human:
6
+
7
+ > **Did we actually build what the spec asked for — no more, no less?**
8
+
9
+ You are NOT the retro agent (that one distils lessons). You are the auditor. Crucially, **you verify
10
+ against the real implementation, not against what the agents claimed.** Transcripts, `learning`
11
+ notes, and even a slice's own status can be optimistic or wrong. The diff cannot. Read the code.
12
+
13
+ ## Input
14
+
15
+ The job payload (stdin JSON) carries:
16
+
17
+ - `variables.planKey` — the epic's key, e.g. `owner/repo#123`.
18
+ - `variables.repo` — the **target repo** `owner/repo` the work landed in.
19
+ - `variables.issueUrl` — the epic's source issue. **This issue body is the SPEC.**
20
+ - **`variables.conformanceDigest`** — appended below the `---` separator: the spec broken into
21
+ slices (each slice's planner-supplied `prompt` is its acceptance brief), the list of **delivered
22
+ PRs** you must examine, and the deviations that were **raised** during implementation
23
+ (`scope-change` entries). This tells you *what to check* and *where to look* — it does NOT tell
24
+ you the answer.
25
+
26
+ You have `gh` / git authenticated for the target repository.
27
+
28
+ ## What to do
29
+
30
+ 1. **Read the spec.** `gh issue view <n> --repo <repo>` for the epic issue body, plus every slice's
31
+ `prompt` in the digest. Together these are the acceptance criteria you are auditing against.
32
+ 2. **Examine the ACTUAL implementation.** For every delivered PR in the digest:
33
+ `gh pr diff <n> --repo <repo>`, and read the touched source and tests (clone/checkout if you
34
+ need to navigate). Do not trust the PR description — read what the code does.
35
+ 3. **Distinguish real delivery from the appearance of it.** This is the whole point of examining
36
+ code. For each spec item, decide whether it is *load-bearing* in the shipped system:
37
+ - Is the new code actually **wired in / reachable**, or is it a dead entrypoint behind a flag,
38
+ a stub, or a synthetic path nothing calls?
39
+ - Do the **tests exercise the real behaviour**, or are they asserting on mocks/gated paths so
40
+ they pass without proving the feature works?
41
+ - Was the item delivered **in full**, or narrowed to a subset while looking complete?
42
+ 4. **Assign each spec item an acceptance verdict**, with a one-line evidence pointer (file / PR /
43
+ test) for each:
44
+ - **met** — delivered as specified, wired in, and genuinely tested.
45
+ - **met-in-unit-only** — implemented and unit-tested, but not proven wired into the live system.
46
+ - **reduced** — delivered in a narrower form than the spec asked for.
47
+ - **not-verified** — you could not confirm it from the implementation (missing, stubbed, dead
48
+ code, or tests that don't actually exercise it).
49
+ 5. **Reconcile the raised deviations.** For each `scope-change` entry in the digest, confirm the
50
+ code matches what was said, and note it as a **raised** deviation.
51
+ 6. **Hunt for UNRAISED deviations** — things the code does that the spec did *not* ask for, or spec
52
+ items silently dropped, that were **never** flagged on the blackboard. These are the most
53
+ valuable finding: scope drift nobody surfaced.
54
+ 7. **Post the conformance report as a comment on the epic issue**
55
+ (`gh issue comment <n> --repo <repo> --body-file <file>`). Structure it: a one-line verdict, a
56
+ per-item table (item → verdict → evidence), the raised deviations, and the unraised deviations.
57
+ Be specific and cite files/PRs — this comment is the deliverable a human reads.
58
+
59
+ ## Do not
60
+
61
+ - Do **not** open, modify, or merge any PR. You audit; you do not remediate. (A later stage decides
62
+ whether a finding warrants a follow-up task.)
63
+ - Do **not** soften a `not-verified` into a `met` because the transcript sounded confident. Absence
64
+ of evidence in the code is `not-verified`.
65
+ - Do **not** manufacture deviations to look thorough. A clean epic that fully met its spec is a
66
+ perfectly good — and common — result.
67
+
68
+ ## Output contract
69
+
70
+ Write a JSON object of **result variables** to the file named by the `AGENT_RESULT_FILE`
71
+ environment variable:
72
+
73
+ ```json
74
+ {
75
+ "status": "filed",
76
+ "commentUrl": "https://github.com/owner/repo/issues/123#issuecomment-456",
77
+ "slicesMet": 4,
78
+ "slicesReduced": 1,
79
+ "slicesNotVerified": 1,
80
+ "deviationsRaised": 2,
81
+ "deviationsUnraised": 1,
82
+ "hasDeviations": true,
83
+ "summary": "6 items: 4 met, 1 reduced (auth rate-limit narrowed to per-IP), 1 not-verified (webhook retry path is dead code). 2 raised + 1 unraised deviation (added a /debug route not in spec)."
84
+ }
85
+ ```
86
+
87
+ Rules:
88
+
89
+ - `status` — one of:
90
+ - `filed` — you examined the implementation and posted the report comment. Set `commentUrl`.
91
+ - `skipped` — there was no landed implementation to examine (nothing shipped). Explain in
92
+ `summary`; omit the counts.
93
+ - `blocked` — you could not proceed (e.g. no read access, could not fetch a PR). Explain in
94
+ `summary`.
95
+ - `commentUrl` — the URL of the report comment you posted on the epic issue, for `filed`.
96
+ - `slicesMet` / `slicesReduced` / `slicesNotVerified` — integer counts of your per-item verdicts
97
+ (count `met-in-unit-only` under `slicesReduced`, since it is not full live delivery).
98
+ - `deviationsRaised` — count of `scope-change` deviations you reconciled.
99
+ - `deviationsUnraised` — count of deviations you found that were never flagged.
100
+ - `hasDeviations` — `true` when anything is reduced / not-verified / an unraised deviation exists;
101
+ i.e. the epic did not cleanly meet its spec. A later stage uses this to decide escalation.
102
+ - `summary` — a short human-readable verdict.
103
+
104
+ You are advisory and post-merge: you gate no delivery. Your value is an honest, code-grounded
105
+ account of what shipped versus what was asked — surface it plainly, and let the humans act on it.
@@ -10,6 +10,11 @@ so the *next* fleet — and human contributors — never re-learn it the hard wa
10
10
  You are the mechanism that lifts a lesson from "a thing one agent happened to hit" to "a thing the
11
11
  repo now tells everyone up front."
12
12
 
13
+ > **Scope:** a separate **conformance** agent runs before you and audits whether the epic met its
14
+ > spec (posting its own report on the epic issue). That is *not* your job — do **not** re-verify
15
+ > acceptance or re-litigate scope here. Focus purely on distilling reusable **lessons** and
16
+ > promoting them into the repo.
17
+
13
18
  ## Input
14
19
 
15
20
  The job payload (stdin JSON) carries:
@@ -0,0 +1,50 @@
1
+ // Unit coverage for pr.conformance-ack — the operator acknowledged a conformance-review escalation
2
+ // (issue #216). It must settle the parked `plan_conformance` row at `review_status = 'reviewed'` so
3
+ // the inbox scan drops it (the retro instance COMPLETES normally, so `instanceTracking.onTerminated`
4
+ // never fires) and fold the operator's disposition note into the audit `summary`.
5
+ import { test } from "node:test";
6
+ import { assertEquals } from "#test-assert";
7
+ import { noopLog } from "../../test/log.ts";
8
+ import handler from "./worker.ts";
9
+
10
+ function fakeApp(rows: Record<string, unknown>[]) {
11
+ const stores: Record<string, Record<string, unknown>[]> = { plan_conformance: rows };
12
+ return {
13
+ data: {
14
+ table(name: string, key: string) {
15
+ const store = (stores[name] ??= []);
16
+ return {
17
+ get: (k: any) => Promise.resolve(store.find((r) => r[key] === k)),
18
+ find: (q: any) => Promise.resolve(store.filter((r) => Object.entries(q).every(([f, v]) => r[f] === v))),
19
+ insert: (row: any) => {
20
+ store.push(row);
21
+ return Promise.resolve(store.length);
22
+ },
23
+ update: (k: any, patch: any) => {
24
+ const row = store.find((r) => r[key] === k);
25
+ if (row) Object.assign(row, patch);
26
+ return Promise.resolve(row);
27
+ },
28
+ };
29
+ },
30
+ },
31
+ log: noopLog(),
32
+ } as any;
33
+ }
34
+
35
+ test("conformance-ack: settles the review at reviewed and appends the operator note to the summary", async () => {
36
+ const rows = [{ plan_key: "owner/repo#7", review_status: "reviewing", summary: "slice 2 reduced" }];
37
+ const app = fakeApp(rows);
38
+ const out = await handler({ variables: { planKey: "owner/repo#7", note: "filed follow-up #9" } } as any, app);
39
+ assertEquals(out, {});
40
+ assertEquals(rows[0].review_status, "reviewed");
41
+ assertEquals(rows[0].summary, "slice 2 reduced\n\nOperator ack: filed follow-up #9");
42
+ });
43
+
44
+ test("conformance-ack: a blank note settles the review without touching the summary", async () => {
45
+ const rows = [{ plan_key: "owner/repo#8", review_status: "reviewing", summary: "auth cache unverified" }];
46
+ const app = fakeApp(rows);
47
+ await handler({ variables: { planKey: "owner/repo#8", note: " " } } as any, app);
48
+ assertEquals(rows[0].review_status, "reviewed");
49
+ assertEquals(rows[0].summary, "auth cache unverified");
50
+ });
@@ -0,0 +1,31 @@
1
+ // pr.conformance-ack — an operator acknowledged a conformance-review escalation (issue #216).
2
+ //
3
+ // When the spec-conformance audit finds the epic did NOT cleanly meet its spec, the `retro` process
4
+ // routes to the `conformance-escalation` operator user task (retro.bpmn), which parks the instance
5
+ // on the operators' inbox with `plan_conformance.review_status = 'reviewing'`. This worker fires once
6
+ // the operator completes that ack task: it settles the row at `reviewed` (so the inbox scan drops it)
7
+ // and folds the operator's optional disposition note into the audit `summary`. The instance then
8
+ // continues to the lessons (retro) synthesis, so the ack is NON-blocking — delivery already landed.
9
+ // Persistence goes through the record gateway (`app.data`), never hand-written SQL.
10
+ import type { AppJobHandler } from "@nanobpm/urban";
11
+ import { acknowledgeConformance } from "../../app/conformance.ts";
12
+
13
+ interface In extends Record<string, unknown> {
14
+ planKey: string;
15
+ note?: unknown;
16
+ }
17
+
18
+ const str = (v: unknown): string | undefined =>
19
+ typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
20
+
21
+ const handler: AppJobHandler<In, Record<string, never>> = async (job, app) => {
22
+ const planKey = job.variables.planKey;
23
+ const note = str(job.variables.note);
24
+
25
+ await acknowledgeConformance(app.data, planKey, note);
26
+ app.log.info("conformance-ack", { planKey, note: note ?? null });
27
+
28
+ return {};
29
+ };
30
+
31
+ export default handler;