its-magic 0.1.2-38 → 0.1.2-39
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.
- package/installer.sh +3 -0
- package/package.json +1 -1
- package/scripts/check_intake_template_parity.py +1 -0
- package/scripts/intake_evidence_lib.py +100 -1
- package/template/.cursor/commands/auto.md +20 -2
- package/template/.cursor/commands/intake.md +29 -2
- package/template/docs/engineering/artifact-ownership-policy.md +1 -1
- package/template/scripts/check_intake_template_parity.py +1 -0
- package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
- package/template/scripts/intake_evidence_lib.py +100 -1
package/installer.sh
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
+
# BUG-0004: keep startup shell options POSIX-safe for /bin/sh execution.
|
|
5
|
+
# Do not use bash-only "set" flags in this unconditional startup path.
|
|
6
|
+
|
|
4
7
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
5
8
|
SOURCE_ROOT="$SCRIPT_DIR/template"
|
|
6
9
|
MANIFEST_NAME="docs/engineering/context/installer-owned-paths.manifest"
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ INTAKE_TEMPLATE_PAIRS: tuple[tuple[str, str], ...] = (
|
|
|
12
12
|
("scripts/intake_evidence_validate.py", "template/scripts/intake_evidence_validate.py"),
|
|
13
13
|
("scripts/intake_evidence_lib.py", "template/scripts/intake_evidence_lib.py"),
|
|
14
14
|
("scripts/intake_bug_routing_guard.py", "template/scripts/intake_bug_routing_guard.py"),
|
|
15
|
+
("scripts/intake_bug_resume_brief_refresh.py", "template/scripts/intake_bug_resume_brief_refresh.py"),
|
|
15
16
|
("scripts/check_intake_template_parity.py", "template/scripts/check_intake_template_parity.py"),
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Deterministic intake evidence validation
|
|
3
|
-
(US-0078 / US-0083 / DEC-0060 / DEC-0067 / R-0055).
|
|
3
|
+
(US-0078 / US-0083 / DEC-0060 / DEC-0067 / R-0055 / BUG-0007 / R-0066).
|
|
4
4
|
|
|
5
5
|
Consumes a logical intake_evidence bundle (dict). PO workflows MUST run this
|
|
6
6
|
gate before mutating backlog/acceptance; failures are fail-closed.
|
|
@@ -202,6 +202,77 @@ def _row_uses_equivalent_evidence(row: dict[str, Any]) -> bool:
|
|
|
202
202
|
return bool(str(row.get("equivalent_evidence_ref") or "").strip())
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def _norm_answer_ref_quoted_user_text(value: Any) -> str:
|
|
206
|
+
"""Normalize quoted_user_text for BUG-0007 duplicate detection (DEC-0060 strip parity)."""
|
|
207
|
+
if value is None:
|
|
208
|
+
return ""
|
|
209
|
+
return str(value).strip()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _row_exempt_from_answer_ref_topic_distinctness(row: dict[str, Any]) -> bool:
|
|
213
|
+
"""BUG-0007: alternate satisfaction paths do not participate in answer_ref blob reuse checks."""
|
|
214
|
+
return _row_uses_equivalent_evidence(row)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _validate_answer_ref_topic_distinctness(
|
|
218
|
+
bundle: dict[str, Any],
|
|
219
|
+
required: list[str],
|
|
220
|
+
by_key: dict[str, dict[str, Any]],
|
|
221
|
+
res: ValidationResult,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
BUG-0007 / R-0066: distinct required topic_key rows must not reuse the same
|
|
225
|
+
quoted_user_text under satisfied_by=answer_ref (normalized), except exempt rows.
|
|
226
|
+
"""
|
|
227
|
+
norm_to_topics: dict[str, list[str]] = {}
|
|
228
|
+
for k in required:
|
|
229
|
+
row = by_key.get(k)
|
|
230
|
+
if not row:
|
|
231
|
+
continue
|
|
232
|
+
sat = (row.get("satisfied_by") or "").strip()
|
|
233
|
+
if sat != "answer_ref":
|
|
234
|
+
continue
|
|
235
|
+
if _row_exempt_from_answer_ref_topic_distinctness(row):
|
|
236
|
+
continue
|
|
237
|
+
irid = _row_run_id(bundle, row)
|
|
238
|
+
tit = _row_turn(row)
|
|
239
|
+
if irid is None or tit is None:
|
|
240
|
+
continue
|
|
241
|
+
qraw = row.get("quoted_user_text")
|
|
242
|
+
qtxt = "" if qraw is None else str(qraw)
|
|
243
|
+
ref = (row.get("ref") or "").strip()
|
|
244
|
+
if not ref or not verify_ie_ref(
|
|
245
|
+
ref,
|
|
246
|
+
intake_run_id=irid,
|
|
247
|
+
turn_index=int(tit),
|
|
248
|
+
topic_key=k,
|
|
249
|
+
satisfied_by=sat,
|
|
250
|
+
quoted_user_text=qtxt,
|
|
251
|
+
):
|
|
252
|
+
continue
|
|
253
|
+
norm = _norm_answer_ref_quoted_user_text(qraw)
|
|
254
|
+
norm_to_topics.setdefault(norm, []).append(k)
|
|
255
|
+
|
|
256
|
+
dup_groups: list[str] = []
|
|
257
|
+
for norm, topics in norm_to_topics.items():
|
|
258
|
+
uniq = sorted(set(topics))
|
|
259
|
+
if len(uniq) < 2:
|
|
260
|
+
continue
|
|
261
|
+
dup_groups.append("text=" + repr(norm[:120]) + " topics=" + ",".join(uniq))
|
|
262
|
+
|
|
263
|
+
if dup_groups:
|
|
264
|
+
res.ok = False
|
|
265
|
+
res.add_code("INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT")
|
|
266
|
+
res.diagnostics.append(
|
|
267
|
+
"Remediation: distinct required topics must not reuse the same quoted_user_text "
|
|
268
|
+
"under satisfied_by=answer_ref (BUG-0007 / R-0066). Duplicates: "
|
|
269
|
+
+ "; ".join(dup_groups)
|
|
270
|
+
+ ". Use per-topic answers, or an allowed alternate path "
|
|
271
|
+
"(evidence_source=equivalent_evidence_ref + equivalent_evidence_ref, "
|
|
272
|
+
"delegation_ref per DEC-0067 / US-0083, or assumption_confirmation_ref on the row)."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
205
276
|
def _candidate_story_ids(bundle: dict[str, Any]) -> set[str]:
|
|
206
277
|
out: set[str] = set()
|
|
207
278
|
|
|
@@ -527,6 +598,8 @@ def validate_intake_evidence(
|
|
|
527
598
|
res.missing_topics.append(k)
|
|
528
599
|
res.missing_topics = sorted(set(res.missing_topics))
|
|
529
600
|
|
|
601
|
+
_validate_answer_ref_topic_distinctness(bundle, required, by_key, res)
|
|
602
|
+
|
|
530
603
|
# US-0081 / DEC-0064: first/new/broad intake requires complete-plan coverage contract.
|
|
531
604
|
if pack == "first-intake-pack":
|
|
532
605
|
_validate_plan_coverage_contract(bundle, res)
|
|
@@ -668,6 +741,32 @@ def self_test() -> None:
|
|
|
668
741
|
assert d0.ok and d1.ok
|
|
669
742
|
assert d0.primary_codes == d1.primary_codes
|
|
670
743
|
|
|
744
|
+
# BUG-0007: same quoted_user_text across multiple answer_ref required topics fails
|
|
745
|
+
dup_txt = "synthetic blob echoed for every topic"
|
|
746
|
+
dup_rows = []
|
|
747
|
+
for i, key in enumerate(small):
|
|
748
|
+
dup_rows.append(
|
|
749
|
+
{
|
|
750
|
+
"topic_key": key,
|
|
751
|
+
"satisfied_by": "answer_ref",
|
|
752
|
+
"quoted_user_text": dup_txt,
|
|
753
|
+
"intake_run_id": rid,
|
|
754
|
+
"turn_index": 300 + i,
|
|
755
|
+
"ref": build_ie_ref(rid, 300 + i, key, "answer_ref", dup_txt),
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
dup_bundle = {
|
|
759
|
+
"selected_pack": "small-intake-pack",
|
|
760
|
+
"intake_run_id": rid,
|
|
761
|
+
"asked_topics": list(small),
|
|
762
|
+
"missing_topics": [],
|
|
763
|
+
"assumptions_confirmed": "(none)",
|
|
764
|
+
"topic_coverage": dup_rows,
|
|
765
|
+
}
|
|
766
|
+
dup_res = validate_intake_evidence(dup_bundle)
|
|
767
|
+
assert not dup_res.ok
|
|
768
|
+
assert "INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT" in dup_res.primary_codes
|
|
769
|
+
|
|
671
770
|
# First-intake full-plan coverage contract (US-0081 / DEC-0064)
|
|
672
771
|
first = PACK_REQUIRED_KEYS["first-intake-pack"]
|
|
673
772
|
first_rows = []
|
|
@@ -9,12 +9,30 @@ description: "its-magic auto: deterministic continuation orchestrator."
|
|
|
9
9
|
- tech-lead
|
|
10
10
|
|
|
11
11
|
## Execution model
|
|
12
|
-
- `/auto` is
|
|
13
|
-
-
|
|
12
|
+
- `/auto` is a **spawn-only orchestrator**: it schedules materialization, spawns
|
|
13
|
+
fresh **phase-role** subagents, and verifies phase boundaries—it **must not**
|
|
14
|
+
execute lifecycle phase work, perform phase-role duties, or author **phase
|
|
15
|
+
deliverables** in the orchestrator context.
|
|
16
|
+
- For each phase, **spawn a fresh subagent** for that phase’s canonical role;
|
|
17
|
+
phase output must arrive only via artifacts and handoff files (no in-turn
|
|
18
|
+
orchestrator execution of that phase).
|
|
14
19
|
- Phase context transfer happens only through artifacts and handoff files.
|
|
15
20
|
- Scope is process/workflow orchestration only. Do not claim runtime product
|
|
16
21
|
orchestration changes.
|
|
17
22
|
|
|
23
|
+
## Spawn-boundary integrity (BUG-0006)
|
|
24
|
+
|
|
25
|
+
- **Forbidden**: treating the orchestrator turn as the executor of a lifecycle
|
|
26
|
+
phase (for example running **`architecture`**, **`execute`**, **`qa`**, or any
|
|
27
|
+
other **`phase_id`** in the orchestrator instead of spawning the required
|
|
28
|
+
subagent).
|
|
29
|
+
- **Fail fast** with **`AUTO_ORCHESTRATOR_PHASE_EXECUTION`**. **Remediation**:
|
|
30
|
+
stop; spawn a **fresh** subagent for the canonical **`phase_id`** and **`role`**
|
|
31
|
+
per the phase→role matrix (**DEC-0051**); do not merge phase output into
|
|
32
|
+
orchestrator turns. **Distinct from** **`PHASE_CONTEXT_ISOLATION_VIOLATION`**
|
|
33
|
+
(wrong writer / isolation break) and **`RUNTIME_PROOF_*`** / **`PHASE_ROLE_*`**
|
|
34
|
+
families—do not overload those codes for a missing-spawn violation.
|
|
35
|
+
|
|
18
36
|
## Full specification (US-0080 / DEC-0062)
|
|
19
37
|
|
|
20
38
|
Long prose, expanded mode semantics, and **Steps 1–13** detail live in
|
|
@@ -21,6 +21,7 @@ description: "its-magic intake: clarify idea and capture story + acceptance."
|
|
|
21
21
|
- `docs/product/backlog.md`
|
|
22
22
|
- `docs/product/acceptance.md`
|
|
23
23
|
- `handoffs/po_to_tl.md`
|
|
24
|
+
- `handoffs/resume_brief.md` (required on successful **`/intake bug`** persistence — **DEC-0069** / **BUG-0005**)
|
|
24
25
|
- Optional (when enabled): `docs/engineering/compatibility-report.md`
|
|
25
26
|
- Optional (when enabled): `docs/engineering/component-scope.md`
|
|
26
27
|
|
|
@@ -41,7 +42,7 @@ description: "its-magic intake: clarify idea and capture story + acceptance."
|
|
|
41
42
|
- For artifact mutations, enforce deterministic single-writer scope:
|
|
42
43
|
- establish writer identity (`writer_id`) and run identity (`intake_run_id`),
|
|
43
44
|
- bind writes to target artifacts (`backlog`, `acceptance`, `vision`,
|
|
44
|
-
`po_to_tl`) for this run.
|
|
45
|
+
`po_to_tl`, and when persisting bugs: `resume_brief` per **DEC-0069**) for this run.
|
|
45
46
|
- Drift guard semantics:
|
|
46
47
|
- self-write changes from the same `(writer_id, intake_run_id)` are valid and
|
|
47
48
|
must not trigger concurrent-writer blockers,
|
|
@@ -73,6 +74,7 @@ description: "its-magic intake: clarify idea and capture story + acceptance."
|
|
|
73
74
|
- `INTAKE_REQUIRED_TOPIC_MISSING`
|
|
74
75
|
- `INTAKE_REQUIRED_PACK_INCOMPLETE`
|
|
75
76
|
- `INTAKE_ASSUMPTION_CONFIRMATION_REQUIRED`
|
|
77
|
+
- `INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT` (**BUG-0007** / **R-0066** — see **Truthfulness** below)
|
|
76
78
|
- `INTAKE_PERSISTENCE_BLOCKED`
|
|
77
79
|
- Remediation guidance surface (mandatory on block):
|
|
78
80
|
- list `missing_topics`,
|
|
@@ -127,6 +129,24 @@ and assumption binding. **Do not** mutate `docs/product/backlog.md` or
|
|
|
127
129
|
(**AC-5**, **AC-6**).
|
|
128
130
|
- **Grandfathering**: legacy intake rows remain valid for read/display; the **next** intake-driven
|
|
129
131
|
mutation must supply full **US-0078** evidence or the write is blocked (**DEC-0060** §5).
|
|
132
|
+
- Truthfulness / anti-echo (**BUG-0007** / **R-0066**): `INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT` when
|
|
133
|
+
the same normalized `quoted_user_text` is reused across distinct required `topic_key` rows under
|
|
134
|
+
`satisfied_by=answer_ref` without an exempt path (`evidence_source=equivalent_evidence_ref` +
|
|
135
|
+
`equivalent_evidence_ref`, `delegation_ref` per **DEC-0067** / **US-0083**, or
|
|
136
|
+
`assumption_confirmation_ref` on the row). Canonical **`ie:`** integrity (**DEC-0060**) does not
|
|
137
|
+
prove a topic was actually elicited.
|
|
138
|
+
|
|
139
|
+
## Truthfulness: `asked_topics` and `topic_coverage` (BUG-0007 / US-0083 / DEC-0060 / DEC-0067)
|
|
140
|
+
|
|
141
|
+
- **`asked_topics`** may list a required `topic_key` only when a **user-visible question** was posed
|
|
142
|
+
**or** a **DEC-0060**-allowed alternate applies: **`delegation_ref`** (**DEC-0067**, **US-0083**),
|
|
143
|
+
**`evidence_source=equivalent_evidence_ref`** with **`equivalent_evidence_ref`**, or
|
|
144
|
+
**`assumption_confirmation_ref`** (row-level and/or bundle-level assumption binding per the gate
|
|
145
|
+
contract above).
|
|
146
|
+
- **Forbidden**: fabricating **`topic_coverage`** by echoing **one** user or bug-report blob into
|
|
147
|
+
**`quoted_user_text`** on **every** required key as **`answer_ref`** solely to satisfy structure.
|
|
148
|
+
The validator rejects that pattern under **`INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT`** (see
|
|
149
|
+
**`docs/engineering/architecture.md`** **`# BUG-0007`**).
|
|
130
150
|
|
|
131
151
|
## Bug issue routing (US-0079 / DEC-0061)
|
|
132
152
|
|
|
@@ -138,6 +158,12 @@ and assumption binding. **Do not** mutate `docs/product/backlog.md` or
|
|
|
138
158
|
- Append **`### BUG-#### — Title`** under **`## Bug issues (canonical)`** with **Status**, **`environment`**, **`steps_to_reproduce`**, **`expected`**, **`actual`**, **`evidence_refs`**.
|
|
139
159
|
- Add matching **`- [ ]` / `- [x]`** row under **`## Bug acceptance (canonical)`**, sorted by id.
|
|
140
160
|
- Run **`python scripts/bug_issue_validate.py --backlog docs/product/backlog.md --check-acceptance`** before completing the handoff.
|
|
161
|
+
- **Resume brief refresh (DEC-0069 / BUG-0005)**: immediately after successful bug persistence and backlog/acceptance validation **PASS**, run the atomic writer (temp file + replace — idempotent latest-pointer upsert):
|
|
162
|
+
- `python scripts/intake_bug_resume_brief_refresh.py --bug-id BUG-#### --backlog docs/product/backlog.md --resume-brief handoffs/resume_brief.md --intake-boundary-utc <RFC3339Z>`
|
|
163
|
+
- Optional: `--orchestrator-run-id`, `--intake-evidence handoffs/intake_evidence/....json`, `--sprint-id` when known.
|
|
164
|
+
- Exit non-zero → **`INTAKE_RESUME_BRIEF_*`** family — do not claim intake complete; fix backlog/brief contradiction or supply valid boundary UTC.
|
|
165
|
+
- Post-condition: **`intended_resume_phase` / `resolved_start_phase` = `discovery`**, **`resolution_source=resume_brief`**, **`bug_id`** matches persisted row, so **`/auto`** without **`start-from`** does not false-trigger **`RESUME_BRIEF_STALE`** for a stale pre-intake **`intake`** target.
|
|
166
|
+
- Optional audit: `python scripts/intake_bug_resume_brief_refresh.py --bug-id BUG-#### --backlog docs/product/backlog.md --resume-brief handoffs/resume_brief.md --validate-file` (no write).
|
|
141
167
|
|
|
142
168
|
## Steps
|
|
143
169
|
1. Determine intake mode from `.cursor/scratchpad.md`:
|
|
@@ -283,7 +309,8 @@ and assumption binding. **Do not** mutate `docs/product/backlog.md` or
|
|
|
283
309
|
- Intake mutations must also comply with
|
|
284
310
|
`docs/engineering/artifact-ownership-policy.md`.
|
|
285
311
|
- Intake may mutate only intake-owned scopes (`vision`, `backlog`, `acceptance`,
|
|
286
|
-
`po_to_tl
|
|
312
|
+
`po_to_tl`, and **`handoffs/resume_brief.md`** only via the **DEC-0069** bug-intake
|
|
313
|
+
completion path / `intake_bug_resume_brief_refresh.py`) for target story context.
|
|
287
314
|
- Any attempted delete/rewrite of non-intake-owned sections fails closed with
|
|
288
315
|
`PHASE_OWNERSHIP_VIOLATION`.
|
|
289
316
|
- If an override-authorized path is configured for an artifact but required
|
|
@@ -23,7 +23,7 @@ artifacts. Ordering policy and ownership policy are complementary:
|
|
|
23
23
|
| `handoffs/release_queue.md` | target sprint row only | `release` | none |
|
|
24
24
|
| `handoffs/release_notes.md` | latest pointer section | `release`, `refresh-context` | none |
|
|
25
25
|
| `docs/engineering/state.md` | append-bottom checkpoints only | all delivery phases | none |
|
|
26
|
-
| `handoffs/resume_brief.md` | current status/next-actions sections | `pause`, `resume`, `refresh-context`, `release` | none |
|
|
26
|
+
| `handoffs/resume_brief.md` | current status/next-actions sections; latest-pointer upsert on **`/intake bug`** completion (**DEC-0069**) | `intake` (bug persistence path only), `pause`, `resume`, `refresh-context`, `release` | none |
|
|
27
27
|
|
|
28
28
|
## Non-destructive mutation rules
|
|
29
29
|
|
|
@@ -12,6 +12,7 @@ INTAKE_TEMPLATE_PAIRS: tuple[tuple[str, str], ...] = (
|
|
|
12
12
|
("scripts/intake_evidence_validate.py", "template/scripts/intake_evidence_validate.py"),
|
|
13
13
|
("scripts/intake_evidence_lib.py", "template/scripts/intake_evidence_lib.py"),
|
|
14
14
|
("scripts/intake_bug_routing_guard.py", "template/scripts/intake_bug_routing_guard.py"),
|
|
15
|
+
("scripts/intake_bug_resume_brief_refresh.py", "template/scripts/intake_bug_resume_brief_refresh.py"),
|
|
15
16
|
("scripts/check_intake_template_parity.py", "template/scripts/check_intake_template_parity.py"),
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Atomic refresh of handoffs/resume_brief.md after successful /intake bug persistence (DEC-0069 / BUG-0005).
|
|
4
|
+
|
|
5
|
+
Idempotent: same inputs yield the same latest-pointer section. Uses temp file + os.replace for atomicity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import bug_issue_lib as bi
|
|
18
|
+
import bug_issue_validate as biv
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def bug_status(backlog_text: str, bug_id: str) -> str | None:
|
|
22
|
+
section = bi.extract_bug_section(backlog_text)
|
|
23
|
+
if not section:
|
|
24
|
+
return None
|
|
25
|
+
for issue in bi.parse_bug_issues(section):
|
|
26
|
+
if issue.bug_id == bug_id:
|
|
27
|
+
return issue.status
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def upsert_latest_orchestration_pointer(full_text: str, latest_block: str) -> str:
|
|
32
|
+
"""Replace ## Latest orchestration pointer section or insert after # Resume Brief."""
|
|
33
|
+
latest_block = latest_block.rstrip("\n") + "\n"
|
|
34
|
+
lines = full_text.splitlines(keepends=True)
|
|
35
|
+
if not lines:
|
|
36
|
+
return "# Resume Brief\n\n" + latest_block
|
|
37
|
+
|
|
38
|
+
out: list[str] = []
|
|
39
|
+
i = 0
|
|
40
|
+
replaced = False
|
|
41
|
+
while i < len(lines):
|
|
42
|
+
line = lines[i]
|
|
43
|
+
if line.startswith("## Latest orchestration pointer"):
|
|
44
|
+
out.append(latest_block)
|
|
45
|
+
i += 1
|
|
46
|
+
while i < len(lines) and not lines[i].startswith("## "):
|
|
47
|
+
i += 1
|
|
48
|
+
replaced = True
|
|
49
|
+
continue
|
|
50
|
+
out.append(line)
|
|
51
|
+
i += 1
|
|
52
|
+
|
|
53
|
+
if replaced:
|
|
54
|
+
return "".join(out)
|
|
55
|
+
|
|
56
|
+
text = "".join(lines)
|
|
57
|
+
stripped = text.lstrip("\n")
|
|
58
|
+
if stripped.startswith("# Resume Brief"):
|
|
59
|
+
return text.rstrip("\n") + "\n\n" + latest_block
|
|
60
|
+
return "# Resume Brief\n\n" + latest_block + text.lstrip("\n")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_latest_pointer_markdown(
|
|
64
|
+
*,
|
|
65
|
+
bug_id: str,
|
|
66
|
+
intake_boundary_utc: str,
|
|
67
|
+
orchestrator_run_id: str | None,
|
|
68
|
+
intake_evidence_ref: str | None,
|
|
69
|
+
sprint_id: str | None,
|
|
70
|
+
) -> str:
|
|
71
|
+
orch = orchestrator_run_id if orchestrator_run_id else "(unknown)"
|
|
72
|
+
ev = intake_evidence_ref if intake_evidence_ref else "(none)"
|
|
73
|
+
spr = sprint_id if sprint_id else "(none)"
|
|
74
|
+
return f"""## Latest orchestration pointer — post-bug-intake (DEC-0069)
|
|
75
|
+
|
|
76
|
+
- **Boundary**: successful **`/intake bug`** persistence (**`US-0045`**) — **`intake_boundary_utc={intake_boundary_utc}`**
|
|
77
|
+
- **`bug_id`**: **`{bug_id}`** — must remain **`OPEN`** in **`docs/product/backlog.md`** (authority); this refresh is rejected if backlog shows **DONE**
|
|
78
|
+
- **Intake evidence ref**: `{ev}`
|
|
79
|
+
- **`orchestrator_run_id`**: `{orch}` (boundary metadata when known; optional at intake)
|
|
80
|
+
- **Contract**: default **`/auto`** continuation targets **`discovery`** for this OPEN bug (not a stale pre-intake **`intake`** resume target)
|
|
81
|
+
|
|
82
|
+
## Current status
|
|
83
|
+
|
|
84
|
+
- **Active bug**: **`{bug_id}`** — **OPEN** per **`docs/product/backlog.md`** at refresh time
|
|
85
|
+
|
|
86
|
+
## Intended resume phase
|
|
87
|
+
|
|
88
|
+
`discovery`
|
|
89
|
+
|
|
90
|
+
## Resume target
|
|
91
|
+
|
|
92
|
+
- bug_id={bug_id}
|
|
93
|
+
- story_id=(none)
|
|
94
|
+
- sprint_id={spr}
|
|
95
|
+
- boundary=post-bug-intake (**DEC-0069**)
|
|
96
|
+
|
|
97
|
+
## Latest auto breadcrumb seed
|
|
98
|
+
|
|
99
|
+
- requested_start_from=(none)
|
|
100
|
+
- resolved_start_phase=discovery
|
|
101
|
+
- resolution_source=resume_brief
|
|
102
|
+
- resolution_status=resolved
|
|
103
|
+
- stop_reason=intake_complete
|
|
104
|
+
- stop_phase=intake
|
|
105
|
+
- next_scheduled_phase=discovery
|
|
106
|
+
- bug_id={bug_id}
|
|
107
|
+
- story_id=(none)
|
|
108
|
+
- sprint_id={spr}
|
|
109
|
+
- orchestrator_run_id={orch}
|
|
110
|
+
- intake_boundary_utc={intake_boundary_utc}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_brief_bug_id(brief_text: str) -> str | None:
|
|
115
|
+
for line in brief_text.splitlines():
|
|
116
|
+
s = line.strip()
|
|
117
|
+
m = re.match(r"^-\s*bug_id=(BUG-\d{4})\s*$", s)
|
|
118
|
+
if m:
|
|
119
|
+
return m.group(1)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_brief_open_bug_alignment(brief_text: str, backlog_text: str) -> list[str]:
|
|
124
|
+
"""Writer-side guard: brief bug_id must match an OPEN row in backlog (US-0045)."""
|
|
125
|
+
errors: list[str] = []
|
|
126
|
+
bid = extract_brief_bug_id(brief_text)
|
|
127
|
+
if not bid:
|
|
128
|
+
errors.append("INTAKE_RESUME_BRIEF_PARSE_BUG_ID_MISSING")
|
|
129
|
+
return errors
|
|
130
|
+
|
|
131
|
+
st = bug_status(backlog_text, bid)
|
|
132
|
+
if st is None:
|
|
133
|
+
errors.append(f"INTAKE_RESUME_BRIEF_BACKLOG_BUG_UNKNOWN:{bid}")
|
|
134
|
+
return errors
|
|
135
|
+
if st != "OPEN":
|
|
136
|
+
errors.append(f"INTAKE_RESUME_BRIEF_BACKLOG_CONTRADICTION:{bid}:status={st}")
|
|
137
|
+
if "`discovery`" not in brief_text and "resolved_start_phase=discovery" not in brief_text:
|
|
138
|
+
errors.append("INTAKE_RESUME_BRIEF_DISCOVERY_PHASE_MISSING")
|
|
139
|
+
return errors
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def atomic_write(path: Path, content: str) -> None:
|
|
143
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
fd, tmp = tempfile.mkstemp(
|
|
145
|
+
dir=path.parent,
|
|
146
|
+
prefix=".resume_brief_tmp_",
|
|
147
|
+
suffix=".md",
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
|
|
151
|
+
f.write(content)
|
|
152
|
+
os.replace(tmp, path)
|
|
153
|
+
except Exception:
|
|
154
|
+
try:
|
|
155
|
+
os.unlink(tmp)
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _fail(code: str, detail: str = "") -> int:
|
|
162
|
+
msg = code
|
|
163
|
+
if detail:
|
|
164
|
+
msg += f": {detail}"
|
|
165
|
+
print(msg, file=sys.stderr)
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def self_test() -> int:
|
|
170
|
+
block = build_latest_pointer_markdown(
|
|
171
|
+
bug_id="BUG-0999",
|
|
172
|
+
intake_boundary_utc="2026-04-03T12:00:00Z",
|
|
173
|
+
orchestrator_run_id="auto-test",
|
|
174
|
+
intake_evidence_ref="handoffs/intake_evidence/x.json",
|
|
175
|
+
sprint_id="S0001",
|
|
176
|
+
)
|
|
177
|
+
old = "# Resume Brief\n\n## Latest orchestration pointer — old\n\n- x\n\n## Checkpoint\n\nkeep\n"
|
|
178
|
+
new = upsert_latest_orchestration_pointer(old, block)
|
|
179
|
+
if "keep" not in new:
|
|
180
|
+
return _fail("SELFTEST_FAILED", "lost tail section")
|
|
181
|
+
if "BUG-0999" not in new or "resolved_start_phase=discovery" not in new:
|
|
182
|
+
return _fail("SELFTEST_FAILED", "missing expected content")
|
|
183
|
+
if "## Latest orchestration pointer — old" in new:
|
|
184
|
+
return _fail("SELFTEST_FAILED", "old latest not replaced")
|
|
185
|
+
good_backlog = """## Bug issues (canonical)
|
|
186
|
+
|
|
187
|
+
### BUG-0999 — T
|
|
188
|
+
- Status: OPEN
|
|
189
|
+
- environment: e
|
|
190
|
+
- steps_to_reproduce: s
|
|
191
|
+
- expected: x
|
|
192
|
+
- actual: y
|
|
193
|
+
- evidence_refs: z
|
|
194
|
+
"""
|
|
195
|
+
errs = validate_brief_open_bug_alignment(new, good_backlog)
|
|
196
|
+
if errs:
|
|
197
|
+
return _fail("SELFTEST_FAILED", str(errs))
|
|
198
|
+
bad_backlog = good_backlog.replace("OPEN", "DONE")
|
|
199
|
+
errs2 = validate_brief_open_bug_alignment(new, bad_backlog)
|
|
200
|
+
if not any("CONTRADICTION" in e for e in errs2):
|
|
201
|
+
return _fail("SELFTEST_FAILED", "expected contradiction on DONE")
|
|
202
|
+
print("[INTAKE_BUG_RESUME_BRIEF_REFRESH_OK]")
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main() -> int:
|
|
207
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
208
|
+
ap.add_argument("--bug-id", default="", help="Persisted BUG-#### id (required unless --self-test)")
|
|
209
|
+
ap.add_argument("--backlog", default="docs/product/backlog.md")
|
|
210
|
+
ap.add_argument("--resume-brief", default="handoffs/resume_brief.md")
|
|
211
|
+
ap.add_argument(
|
|
212
|
+
"--intake-boundary-utc",
|
|
213
|
+
default="",
|
|
214
|
+
help="RFC3339 UTC timestamp for intake completion boundary (required unless --validate-file)",
|
|
215
|
+
)
|
|
216
|
+
ap.add_argument("--orchestrator-run-id", default="", help="Optional orchestrator run id")
|
|
217
|
+
ap.add_argument("--intake-evidence", default="", help="Optional intake evidence path or ref")
|
|
218
|
+
ap.add_argument("--sprint-id", default="", help="Optional sprint id")
|
|
219
|
+
ap.add_argument("--dry-run", action="store_true", help="Print body only; do not write")
|
|
220
|
+
ap.add_argument("--validate-file", action="store_true", help="Validate existing brief vs backlog; no write")
|
|
221
|
+
ap.add_argument("--self-test", action="store_true")
|
|
222
|
+
args = ap.parse_args()
|
|
223
|
+
if args.self_test:
|
|
224
|
+
return self_test()
|
|
225
|
+
|
|
226
|
+
if not args.bug_id:
|
|
227
|
+
return _fail("INTAKE_RESUME_BRIEF_INVALID_BUG_ID", "missing --bug-id")
|
|
228
|
+
if not re.fullmatch(r"BUG-\d{4}", args.bug_id):
|
|
229
|
+
return _fail("INTAKE_RESUME_BRIEF_INVALID_BUG_ID", args.bug_id)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
backlog_text = Path(args.backlog).read_text(encoding="utf-8")
|
|
233
|
+
except OSError as e:
|
|
234
|
+
return _fail("INTAKE_RESUME_BRIEF_IO_ERROR", str(e))
|
|
235
|
+
|
|
236
|
+
berr, _ = biv.validate_backlog(backlog_text)
|
|
237
|
+
if berr:
|
|
238
|
+
for e in berr:
|
|
239
|
+
print(e, file=sys.stderr)
|
|
240
|
+
return _fail("INTAKE_RESUME_BRIEF_BACKLOG_INVALID", berr[0])
|
|
241
|
+
|
|
242
|
+
resume_path = Path(args.resume_brief)
|
|
243
|
+
if args.validate_file:
|
|
244
|
+
if not resume_path.is_file():
|
|
245
|
+
return _fail("INTAKE_RESUME_BRIEF_MISSING", str(resume_path))
|
|
246
|
+
brief_text = resume_path.read_text(encoding="utf-8")
|
|
247
|
+
bid_file = extract_brief_bug_id(brief_text)
|
|
248
|
+
if bid_file != args.bug_id:
|
|
249
|
+
return _fail(
|
|
250
|
+
"INTAKE_RESUME_BRIEF_BUG_ID_MISMATCH",
|
|
251
|
+
f"cli={args.bug_id} brief={bid_file}",
|
|
252
|
+
)
|
|
253
|
+
verr = validate_brief_open_bug_alignment(brief_text, backlog_text)
|
|
254
|
+
if verr:
|
|
255
|
+
for e in verr:
|
|
256
|
+
print(e, file=sys.stderr)
|
|
257
|
+
return 1
|
|
258
|
+
print("[INTAKE_RESUME_BRIEF_VALIDATE_OK]")
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
if not args.intake_boundary_utc.strip():
|
|
262
|
+
return _fail("INTAKE_RESUME_BRIEF_BOUNDARY_UTC_REQUIRED", "supply --intake-boundary-utc")
|
|
263
|
+
|
|
264
|
+
st = bug_status(backlog_text, args.bug_id)
|
|
265
|
+
if st is None:
|
|
266
|
+
return _fail("INTAKE_RESUME_BRIEF_BUG_NOT_FOUND", args.bug_id)
|
|
267
|
+
if st != "OPEN":
|
|
268
|
+
return _fail(
|
|
269
|
+
"INTAKE_RESUME_BRIEF_BACKLOG_CONTRADICTION",
|
|
270
|
+
f"{args.bug_id} must be OPEN for discovery default; got {st}",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
block = build_latest_pointer_markdown(
|
|
274
|
+
bug_id=args.bug_id,
|
|
275
|
+
intake_boundary_utc=args.intake_boundary_utc,
|
|
276
|
+
orchestrator_run_id=args.orchestrator_run_id or None,
|
|
277
|
+
intake_evidence_ref=args.intake_evidence or None,
|
|
278
|
+
sprint_id=args.sprint_id or None,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
prior = resume_path.read_text(encoding="utf-8") if resume_path.is_file() else ""
|
|
282
|
+
merged = upsert_latest_orchestration_pointer(prior, block)
|
|
283
|
+
verr = validate_brief_open_bug_alignment(merged, backlog_text)
|
|
284
|
+
if verr:
|
|
285
|
+
for e in verr:
|
|
286
|
+
print(e, file=sys.stderr)
|
|
287
|
+
return 1
|
|
288
|
+
|
|
289
|
+
if args.dry_run:
|
|
290
|
+
print(merged)
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
atomic_write(resume_path, merged)
|
|
295
|
+
except OSError as e:
|
|
296
|
+
return _fail("INTAKE_RESUME_BRIEF_WRITE_FAILED", str(e))
|
|
297
|
+
|
|
298
|
+
print("[INTAKE_BUG_RESUME_BRIEF_REFRESH_OK]")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
raise SystemExit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Deterministic intake evidence validation
|
|
3
|
-
(US-0078 / US-0083 / DEC-0060 / DEC-0067 / R-0055).
|
|
3
|
+
(US-0078 / US-0083 / DEC-0060 / DEC-0067 / R-0055 / BUG-0007 / R-0066).
|
|
4
4
|
|
|
5
5
|
Consumes a logical intake_evidence bundle (dict). PO workflows MUST run this
|
|
6
6
|
gate before mutating backlog/acceptance; failures are fail-closed.
|
|
@@ -202,6 +202,77 @@ def _row_uses_equivalent_evidence(row: dict[str, Any]) -> bool:
|
|
|
202
202
|
return bool(str(row.get("equivalent_evidence_ref") or "").strip())
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def _norm_answer_ref_quoted_user_text(value: Any) -> str:
|
|
206
|
+
"""Normalize quoted_user_text for BUG-0007 duplicate detection (DEC-0060 strip parity)."""
|
|
207
|
+
if value is None:
|
|
208
|
+
return ""
|
|
209
|
+
return str(value).strip()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _row_exempt_from_answer_ref_topic_distinctness(row: dict[str, Any]) -> bool:
|
|
213
|
+
"""BUG-0007: alternate satisfaction paths do not participate in answer_ref blob reuse checks."""
|
|
214
|
+
return _row_uses_equivalent_evidence(row)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _validate_answer_ref_topic_distinctness(
|
|
218
|
+
bundle: dict[str, Any],
|
|
219
|
+
required: list[str],
|
|
220
|
+
by_key: dict[str, dict[str, Any]],
|
|
221
|
+
res: ValidationResult,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
BUG-0007 / R-0066: distinct required topic_key rows must not reuse the same
|
|
225
|
+
quoted_user_text under satisfied_by=answer_ref (normalized), except exempt rows.
|
|
226
|
+
"""
|
|
227
|
+
norm_to_topics: dict[str, list[str]] = {}
|
|
228
|
+
for k in required:
|
|
229
|
+
row = by_key.get(k)
|
|
230
|
+
if not row:
|
|
231
|
+
continue
|
|
232
|
+
sat = (row.get("satisfied_by") or "").strip()
|
|
233
|
+
if sat != "answer_ref":
|
|
234
|
+
continue
|
|
235
|
+
if _row_exempt_from_answer_ref_topic_distinctness(row):
|
|
236
|
+
continue
|
|
237
|
+
irid = _row_run_id(bundle, row)
|
|
238
|
+
tit = _row_turn(row)
|
|
239
|
+
if irid is None or tit is None:
|
|
240
|
+
continue
|
|
241
|
+
qraw = row.get("quoted_user_text")
|
|
242
|
+
qtxt = "" if qraw is None else str(qraw)
|
|
243
|
+
ref = (row.get("ref") or "").strip()
|
|
244
|
+
if not ref or not verify_ie_ref(
|
|
245
|
+
ref,
|
|
246
|
+
intake_run_id=irid,
|
|
247
|
+
turn_index=int(tit),
|
|
248
|
+
topic_key=k,
|
|
249
|
+
satisfied_by=sat,
|
|
250
|
+
quoted_user_text=qtxt,
|
|
251
|
+
):
|
|
252
|
+
continue
|
|
253
|
+
norm = _norm_answer_ref_quoted_user_text(qraw)
|
|
254
|
+
norm_to_topics.setdefault(norm, []).append(k)
|
|
255
|
+
|
|
256
|
+
dup_groups: list[str] = []
|
|
257
|
+
for norm, topics in norm_to_topics.items():
|
|
258
|
+
uniq = sorted(set(topics))
|
|
259
|
+
if len(uniq) < 2:
|
|
260
|
+
continue
|
|
261
|
+
dup_groups.append("text=" + repr(norm[:120]) + " topics=" + ",".join(uniq))
|
|
262
|
+
|
|
263
|
+
if dup_groups:
|
|
264
|
+
res.ok = False
|
|
265
|
+
res.add_code("INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT")
|
|
266
|
+
res.diagnostics.append(
|
|
267
|
+
"Remediation: distinct required topics must not reuse the same quoted_user_text "
|
|
268
|
+
"under satisfied_by=answer_ref (BUG-0007 / R-0066). Duplicates: "
|
|
269
|
+
+ "; ".join(dup_groups)
|
|
270
|
+
+ ". Use per-topic answers, or an allowed alternate path "
|
|
271
|
+
"(evidence_source=equivalent_evidence_ref + equivalent_evidence_ref, "
|
|
272
|
+
"delegation_ref per DEC-0067 / US-0083, or assumption_confirmation_ref on the row)."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
205
276
|
def _candidate_story_ids(bundle: dict[str, Any]) -> set[str]:
|
|
206
277
|
out: set[str] = set()
|
|
207
278
|
|
|
@@ -527,6 +598,8 @@ def validate_intake_evidence(
|
|
|
527
598
|
res.missing_topics.append(k)
|
|
528
599
|
res.missing_topics = sorted(set(res.missing_topics))
|
|
529
600
|
|
|
601
|
+
_validate_answer_ref_topic_distinctness(bundle, required, by_key, res)
|
|
602
|
+
|
|
530
603
|
# US-0081 / DEC-0064: first/new/broad intake requires complete-plan coverage contract.
|
|
531
604
|
if pack == "first-intake-pack":
|
|
532
605
|
_validate_plan_coverage_contract(bundle, res)
|
|
@@ -668,6 +741,32 @@ def self_test() -> None:
|
|
|
668
741
|
assert d0.ok and d1.ok
|
|
669
742
|
assert d0.primary_codes == d1.primary_codes
|
|
670
743
|
|
|
744
|
+
# BUG-0007: same quoted_user_text across multiple answer_ref required topics fails
|
|
745
|
+
dup_txt = "synthetic blob echoed for every topic"
|
|
746
|
+
dup_rows = []
|
|
747
|
+
for i, key in enumerate(small):
|
|
748
|
+
dup_rows.append(
|
|
749
|
+
{
|
|
750
|
+
"topic_key": key,
|
|
751
|
+
"satisfied_by": "answer_ref",
|
|
752
|
+
"quoted_user_text": dup_txt,
|
|
753
|
+
"intake_run_id": rid,
|
|
754
|
+
"turn_index": 300 + i,
|
|
755
|
+
"ref": build_ie_ref(rid, 300 + i, key, "answer_ref", dup_txt),
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
dup_bundle = {
|
|
759
|
+
"selected_pack": "small-intake-pack",
|
|
760
|
+
"intake_run_id": rid,
|
|
761
|
+
"asked_topics": list(small),
|
|
762
|
+
"missing_topics": [],
|
|
763
|
+
"assumptions_confirmed": "(none)",
|
|
764
|
+
"topic_coverage": dup_rows,
|
|
765
|
+
}
|
|
766
|
+
dup_res = validate_intake_evidence(dup_bundle)
|
|
767
|
+
assert not dup_res.ok
|
|
768
|
+
assert "INTAKE_ANSWER_REF_NOT_TOPIC_DISTINCT" in dup_res.primary_codes
|
|
769
|
+
|
|
671
770
|
# First-intake full-plan coverage contract (US-0081 / DEC-0064)
|
|
672
771
|
first = PACK_REQUIRED_KEYS["first-intake-pack"]
|
|
673
772
|
first_rows = []
|