qfai 2.1.1 → 2.1.2

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 (134) hide show
  1. package/README.md +56 -36
  2. package/assets/defaults/agent-routing/qfai-triage.yml +9 -0
  3. package/assets/defaults/workflows/run-owned-cleanup.yml +12 -0
  4. package/assets/init/.claude/settings.json +27 -9
  5. package/assets/init/.codex/hooks.json +20 -8
  6. package/assets/init/.qfai/assistant/agent/frontend-engineer.md +5 -0
  7. package/assets/init/.qfai/assistant/agent/product-experience-architect.md +4 -0
  8. package/assets/init/.qfai/assistant/agent/product-surface-reviewer.md +7 -0
  9. package/assets/init/.qfai/assistant/agent/requirements-analyst.md +8 -0
  10. package/assets/init/.qfai/assistant/rule/communication.md +18 -8
  11. package/assets/init/.qfai/assistant/rule/drift-protocol.md +58 -2
  12. package/assets/init/.qfai/assistant/rule/references/asset-authoring.md +6 -4
  13. package/assets/init/.qfai/assistant/rule/references/worker-edit-boundary.md +48 -0
  14. package/assets/init/.qfai/assistant/rule/shared-skill-delegation-baseline.md +15 -25
  15. package/assets/init/.qfai/assistant/rule/shared-skill-operating-baseline.md +8 -8
  16. package/assets/init/.qfai/assistant/rule/ui-definition-protocol.md +6 -1
  17. package/assets/init/.qfai/assistant/skill/qfai-configure/SKILL.md +3 -3
  18. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/discussion-completion-matrix.md +3 -7
  19. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui-bearing-playbook.md +2 -4
  20. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux_best_practices.md +1 -6
  21. package/assets/init/.qfai/assistant/skill/qfai-discussion/templates/uiux/00_index.md +0 -16
  22. package/assets/init/.qfai/assistant/skill/qfai-implement/SKILL.md +9 -3
  23. package/assets/init/.qfai/assistant/skill/qfai-implement/references/parallelization-policy.md +9 -1
  24. package/assets/init/.qfai/assistant/skill/qfai-implement/references/red-admissibility.md +46 -0
  25. package/assets/init/.qfai/assistant/skill/qfai-implement/references/ui-affecting.md +3 -0
  26. package/assets/init/.qfai/assistant/skill/qfai-maintain/SKILL.md +3 -3
  27. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/SKILL.md +12 -6
  28. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-guide.md +1 -265
  29. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-placement.md +273 -0
  30. package/assets/init/.qfai/assistant/skill/qfai-prototyping/SKILL.md +4 -3
  31. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/review-payload-schema.md +2 -3
  32. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/reviewer-prompt.md +17 -1
  33. package/assets/init/.qfai/assistant/skill/qfai-run/SKILL.md +30 -17
  34. package/assets/init/.qfai/assistant/skill/qfai-run/references/evidence-delivery.md +322 -0
  35. package/assets/init/.qfai/assistant/skill/qfai-run/references/extraction.md +47 -24
  36. package/assets/init/.qfai/assistant/skill/qfai-run/references/operator-screens.md +218 -11
  37. package/assets/init/.qfai/assistant/skill/qfai-run/references/plan.md +4 -3
  38. package/assets/init/.qfai/assistant/skill/qfai-run/references/stage-points.md +14 -0
  39. package/assets/init/.qfai/assistant/skill/qfai-sdd/SKILL.md +4 -3
  40. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/contract-artifact-rules.md +1 -0
  41. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-anti-patterns.md +23 -5
  42. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/requirements-decomposition.md +8 -0
  43. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-execution-playbook.md +29 -4
  44. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-triage.md +132 -4
  45. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/spec-traceability-rules.md +14 -2
  46. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md +114 -0
  47. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-design-contract-normalization.md +4 -29
  48. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/change-request.md +38 -1
  49. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/contracts/ui-contract.sample.yaml +52 -5
  50. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/decisions.md +3 -0
  51. package/assets/init/.qfai/assistant/skill/qfai-triage/SKILL.md +29 -19
  52. package/assets/init/.qfai/assistant/skill/qfai-verify/SKILL.md +5 -4
  53. package/assets/init/.qfai/assistant/skill/qfai-verify/references/context-load.md +9 -6
  54. package/assets/init/.qfai/assistant/skill/qfai-verify/references/verify-output-contract.md +72 -7
  55. package/assets/init/.qfai/assistant/skill/web-research/SKILL.md +35 -21
  56. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/brave-search/config.toml +2 -4
  57. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/brave-search/mcp-config.json +9 -5
  58. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/.mcp.json +1 -2
  59. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/config.toml +3 -3
  60. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/mcp-config.json +11 -8
  61. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/playwright/config.toml +1 -1
  62. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/playwright/mcp-config.json +7 -3
  63. package/assets/init/.qfai/assistant/step/common-design-md/STEP.md +0 -4
  64. package/assets/init/.qfai/assistant/step/common-gate-run/STEP.md +98 -8
  65. package/assets/init/.qfai/assistant/step/common-review-cycle/STEP.md +3 -2
  66. package/assets/init/.qfai/assistant/step/discussion-pack/STEP.md +1 -0
  67. package/assets/init/.qfai/assistant/step/discussion-research/STEP.md +2 -0
  68. package/assets/init/.qfai/assistant/step/discussion-uiux/STEP.md +1 -3
  69. package/assets/init/.qfai/assistant/step/implement-backport/STEP.md +1 -1
  70. package/assets/init/.qfai/assistant/step/implement-benchmark/STEP.md +7 -4
  71. package/assets/init/.qfai/assistant/step/implement-dep-bump/STEP.md +4 -1
  72. package/assets/init/.qfai/assistant/step/implement-diagnose/STEP.md +2 -0
  73. package/assets/init/.qfai/assistant/step/implement-oracle-parity/STEP.md +2 -2
  74. package/assets/init/.qfai/assistant/step/implement-quarantine/STEP.md +3 -3
  75. package/assets/init/.qfai/assistant/step/implement-refactor/STEP.md +2 -2
  76. package/assets/init/.qfai/assistant/step/implement-regression-fix/STEP.md +4 -3
  77. package/assets/init/.qfai/assistant/step/implement-retire/STEP.md +6 -3
  78. package/assets/init/.qfai/assistant/step/implement-revert/STEP.md +4 -3
  79. package/assets/init/.qfai/assistant/step/implement-seam/STEP.md +6 -5
  80. package/assets/init/.qfai/assistant/step/implement-stress-harness/STEP.md +1 -1
  81. package/assets/init/.qfai/assistant/step/implement-sweep/STEP.md +4 -3
  82. package/assets/init/.qfai/assistant/step/implement-tdd/STEP.md +39 -5
  83. package/assets/init/.qfai/assistant/step/implement-tooling/STEP.md +22 -2
  84. package/assets/init/.qfai/assistant/step/maintain-edit/STEP.md +18 -14
  85. package/assets/init/.qfai/assistant/step/prototyping-grill/STEP.md +4 -3
  86. package/assets/init/.qfai/assistant/step/prototyping-loop/STEP.md +4 -0
  87. package/assets/init/.qfai/assistant/step/prototyping-preflight/STEP.md +1 -3
  88. package/assets/init/.qfai/assistant/step/sdd-contract/STEP.md +11 -0
  89. package/assets/init/.qfai/assistant/step/sdd-flow/STEP.md +2 -0
  90. package/assets/init/.qfai/assistant/step/sdd-story/STEP.md +10 -0
  91. package/assets/init/.qfai/assistant/step/sdd-triage/STEP.md +30 -8
  92. package/assets/init/.qfai/assistant/step/triage-answer/STEP.md +5 -4
  93. package/assets/init/.qfai/assistant/step/triage-close/STEP.md +33 -23
  94. package/assets/init/.qfai/assistant/step/triage-cluster/STEP.md +5 -3
  95. package/assets/init/.qfai/assistant/step/triage-decompose/STEP.md +2 -0
  96. package/assets/init/.qfai/assistant/step/triage-dedupe/STEP.md +4 -0
  97. package/assets/init/.qfai/assistant/step/triage-handoff/STEP.md +13 -4
  98. package/assets/init/.qfai/assistant/step/triage-investigate/STEP.md +8 -5
  99. package/assets/init/.qfai/assistant/step/triage-owned-cleanup/STEP.md +86 -0
  100. package/assets/init/.qfai/assistant/step/triage-request-info/STEP.md +2 -0
  101. package/assets/init/.qfai/assistant/step/triage-security-intake/STEP.md +6 -3
  102. package/assets/init/.qfai/assistant/step/verify-change-note/STEP.md +1 -0
  103. package/assets/init/.qfai/assistant/step/verify-commit/STEP.md +37 -9
  104. package/assets/init/.qfai/assistant/step/verify-external/STEP.md +5 -2
  105. package/assets/init/.qfai/assistant/step/verify-qfai-gate/STEP.md +7 -16
  106. package/assets/init/.qfai/assistant/step/verify-release-notes/STEP.md +1 -0
  107. package/assets/init/.qfai/assistant/step/verify-repeat-run/STEP.md +6 -4
  108. package/assets/init/.qfai/assistant/step/verify-repo-gate/STEP.md +121 -30
  109. package/assets/init/root/.agents/rules/ai-readable-markdown.md +2 -2
  110. package/assets/init/root/.agents/rules/api-budget.md +187 -8
  111. package/assets/init/root/.agents/rules/documentation-clarity.md +162 -33
  112. package/assets/init/root/.agents/rules/minimal-implementation.md +3 -3
  113. package/assets/init/root/.agents/rules/reminders.json +27 -14
  114. package/assets/init/root/.agents/rules/session-feedback.md +136 -0
  115. package/assets/init/root/.agents/rules/temporary-files.md +33 -0
  116. package/assets/init/root/.agents/rules/user-questions.md +48 -37
  117. package/assets/init/root/.github/workflows/qfai-docs.yml +1 -1
  118. package/assets/init/root/AGENTS.md +23 -22
  119. package/assets/sandbox-templates/default-deny.yaml +9 -3
  120. package/assets/schemas/workflow/extraction.schema.json +85 -5
  121. package/assets/uix-rev/comparison-review.md +1 -1
  122. package/assets/uix-rev/contracts-review.md +1 -2
  123. package/dist/cli/index.cjs +6697 -8810
  124. package/dist/cli/index.cjs.map +1 -1
  125. package/dist/cli/index.mjs +6036 -8152
  126. package/dist/cli/index.mjs.map +1 -1
  127. package/dist/index.cjs +4947 -7746
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.cts +18 -658
  130. package/dist/index.d.ts +18 -658
  131. package/dist/index.mjs +4866 -7640
  132. package/dist/index.mjs.map +1 -1
  133. package/package.json +4 -6
  134. package/assets/init/root/CLAUDE.md +0 -46
package/README.md CHANGED
@@ -79,11 +79,21 @@ Then open your AI coding agent in the repository and describe the change in your
79
79
 
80
80
  > Let each customer register up to five notification addresses, with no duplicates.
81
81
 
82
+ If you only have an idea, say so: the run starts with a discussion that structures scope and open questions.
83
+
82
84
  The agent announces the goal, the stages it will run and the files it may change, then runs them.
83
85
  It asks you only what it cannot decide for you, such as whether to create a new story.
86
+ Answer its questions, or say `stop` to end the run.
84
87
  The stages fill the seeded story tree and follow the project's Standard commands in
85
88
  `<paths.contractsDir>/tech.md` (by default `.qfai/spec/03_contract/tech.md`) for its quality gates.
86
89
 
90
+ When the run ends, check the result:
91
+
92
+ ```bash
93
+ npx qfai validate
94
+ npx qfai report
95
+ ```
96
+
87
97
  To drive the stages yourself instead:
88
98
  Run `/qfai-discussion` and `/qfai-sdd` to fill the seeded story tree.
89
99
  See [Invoking a stage directly](#invoking-a-stage-directly-expert-path).
@@ -313,27 +323,22 @@ flowchart LR
313
323
  (`assistant/rule/`, `skill/`, `agent/` and `prompt/`), plus `qfai.config.yaml`.
314
324
  - Options:
315
325
 
316
- | Flag | Effect |
317
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
318
- | `--dir <path>` | Output directory (default: the current directory). Wins over `--root` when both are given. |
319
- | `--root <path>` | Every other command reads this as the target directory; `init` reads it as the output directory too, but only when `--dir` is omitted. |
320
- | `--force` | Overwrite the shipped skills, steps, agents and rules, the rule masters under `.agents/rules/`, their host wrappers, and generated Copilot instructions, whether or not you edited them. Project content, `qfai.config.yaml`, `DESIGN.md`, `AGENTS.md`, `CLAUDE.md` and routing overrides are preserved. The managed `.gitignore` block and `core.symlinks` setting are repaired on every non-dry-run. |
321
- | `--dry-run` | Report what would change and write nothing. Use it to rehearse `--upgrade-assistant-tree`. |
322
- | `--upgrade-assistant-tree` | Copy recognized legacy assistant files into the singular tree without deleting a source or overwriting a destination. Migrate old spec packs with `/qfai-migration-v1-to-v2`. Unrecognized assistant files stay in place. |
323
- | `--yes` | Reserved for a future interactive mode; no behavioural difference today. |
324
- | `--verbose` | Expand the run report's `skipped` list to the full path listing. Off by default, so a no-op re-run prints the skip count and a pointer to this flag instead of every shipped asset path. It does not gate the written or removed listings: those are printed whenever they have entries, with or without this flag. |
325
- | `--help`, `-h` | Print the CLI usage banner and exit without writing anything. Accepted by every command, `init` included, and handled before the command runs. |
326
- | `--version`, `-V` | Print the installed QFAI version to stdout and exit 0. Accepted by every command, `init` included, and handled before the command runs, so it works outside a project too. |
327
-
328
- - `QFAI-DEPRECATED-001` means the legacy assistant layout remains past its
329
- supported window. Run `npx qfai init --upgrade-assistant-tree` to copy
330
- recognized files into the current tree without deleting their sources.
326
+ | Flag | Effect |
327
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
328
+ | `--dir <path>` | Output directory (default: the current directory). Wins over `--root` when both are given. |
329
+ | `--root <path>` | Every other command reads this as the target directory; `init` reads it as the output directory too, but only when `--dir` is omitted. |
330
+ | `--force` | Overwrite the shipped skills, steps, agents and rules, the rule masters under `.agents/rules/`, their host wrappers, and generated Copilot instructions, whether or not you edited them. Project content, `qfai.config.yaml`, `DESIGN.md`, `AGENTS.md` and routing overrides are preserved. The managed `.gitignore` block and `core.symlinks` setting are repaired on every non-dry-run. |
331
+ | `--dry-run` | Report what would change and write nothing. |
332
+ | `--yes` | Reserved for a future interactive mode; no behavioural difference today. |
333
+ | `--verbose` | Expand the run report's `skipped` list to the full path listing. Off by default, so a no-op re-run prints the skip count and a pointer to this flag instead of every shipped asset path. It does not gate the written or removed listings: those are printed whenever they have entries, with or without this flag. |
334
+ | `--help`, `-h` | Print the CLI usage banner and exit without writing anything. Accepted by every command, `init` included, and handled before the command runs. |
335
+ | `--version`, `-V` | Print the installed QFAI version to stdout and exit 0. Accepted by every command, `init` included, and handled before the command runs, so it works outside a project too. |
331
336
 
332
337
  - `npx qfai validate`
333
338
  - Validates the story tree, contracts and test obligations, writes `.qfai/report/validate.json`,
334
339
  and appends run logs to `.qfai/report/run-*/`; use `--fail-on error` (or `--fail-on warning`) to turn it into a CI gate,
335
340
  and `--format github` to emit GitHub-friendly annotations.
336
- Use `--flow BF-0001` to scope a run to one business flow. `--spec` is retired.
341
+ Use `--flow BF-0001` to scope a run to one business flow.
337
342
  Use `--profile discussion|sdd|prototyping|atdd|tdd|verify` for local skill-owned checks; CI should use default/full validation (or `verify` / `tdd` for the dedicated CI gates).
338
343
  - `npx qfai report`
339
344
  - Produces a human-readable report (`report.md` by default) or an internal JSON export (`report.json`) from `validate.json`; use `--base-url` to link file paths in Markdown to your repository viewer.
@@ -426,15 +431,29 @@ Notes.
426
431
  issue order are not stable; match on `issues[].code`.
427
432
  - `report.json`, `doctor.json`, and `run-*` JSON logs are internal exports and are not a stable external contract; prefer `report.md` for integrations that must survive tool upgrades.
428
433
 
429
- ## Minimal tutorial
434
+ ## Dependency security
430
435
 
431
- 1. `npx qfai init`
432
- 2. Open your AI coding agent in the repository and describe the change in your own words.
433
- If you only have an idea, say so: the run starts with a discussion that structures scope and open questions.
434
- 3. Answer the questions the work puts to you, or say `stop` to end it.
435
- 4. Run `npx qfai validate` then `npx qfai report`.
436
+ The glob-matching dependency `braces` has an unresolved
437
+ [stack-exhaustion denial-of-service advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm).
438
+ QFAI accepts this residual risk only for reviewed project patterns and paths,
439
+ trusted configuration and trusted SDK callers. This is risk acceptance, not a
440
+ clean audit or proof that QFAI is unaffected. Reproduction and exploitation in
441
+ QFAI remain unverified.
436
442
 
437
- To choose each stage yourself, see [Invoking a stage directly](#invoking-a-stage-directly-expert-path).
443
+ Review include, ignore and negative patterns, constructed paths and SDK inputs.
444
+ `validateProject` accepts a caller-supplied `ConfigLoadResult` without invoking
445
+ the configuration loader; the caller must review those values too. Catching
446
+ errors and limiting results after glob expansion do not defend against stack
447
+ exhaustion during expansion.
448
+
449
+ Untrusted pull requests, externally supplied configuration and service request
450
+ inputs are outside this acceptance. Review those inputs and restrict access;
451
+ do not expose glob processing to them until your mitigation and security review
452
+ cover them.
453
+
454
+ Reassess the risk when a compatible patch becomes available or untrusted input
455
+ is needed. A forced downgrade to `qfai@0.4.0` is not a compatible fix for the
456
+ current major API; do not apply it through `npm audit fix --force`.
438
457
 
439
458
  ## FAQ
440
459
 
@@ -607,6 +626,17 @@ Waiver policy.
607
626
  - Expired waivers are reported as warnings (`QFAI-WAIVER-003`) and must be renewed or removed with evidence.
608
627
  - Suppressed findings remain visible in reports as `suppressed=true`; waivers do not erase findings.
609
628
 
629
+ When upgrading a project with `.qfai/waivers.yml`, back up that file. Check the
630
+ current public finding codes and the release notes for confirmed removals or
631
+ renames. Update a `rule:` value only when the documented rename preserves the
632
+ finding's meaning and scope. Remove confirmed retired or inapplicable waivers,
633
+ keeping the backup and recording each removal's reason for the operator. Do not
634
+ rename whole code prefixes or automatically delete an unknown rule.
635
+
636
+ Rerun full validation and require zero errors. An unknown waiver rule produces
637
+ `QFAI-WAIVER-004` at `warning` and is not applied in that run; investigate it
638
+ against the current diagnostics and release notes.
639
+
610
640
  Typical customizations.
611
641
 
612
642
  - Add a `doctor` step before validate if you want to fail fast on path/glob/config issues.
@@ -929,7 +959,6 @@ commit that bumps the package, and to keep the two from being merged separately.
929
959
  │ │ └── open-questions.md
930
960
  │ └── waivers.yml
931
961
  ├── AGENTS.md
932
- ├── CLAUDE.md
933
962
  └── qfai.config.yaml
934
963
  ```
935
964
 
@@ -966,17 +995,6 @@ provide those defaults to the agent.
966
995
  The `.claude` / `.github` agent wrappers are symlinks and follow the canonical document
967
996
  automatically; the Codex profiles are generated files, so rerun `npx qfai init --force`
968
997
  to refresh them (and any other wrapper asset that has drifted).
969
- `--force` deletes as well as overwrites: it removes the command and prompt wrappers
970
- under `.claude/commands/` and `.github/prompts/`, and the skill wrappers under
971
- `.claude/skills/`, `.agents/skills/`, `.codex/skills/` and `.github/skills/` for skills that are
972
- not in the current release, whether those wrappers are directories or symlinks.
973
- Files it did not write — a project's own slash command, prompt file or skill,
974
- including one published from a project-authored `.qfai/assistant/skill/` entry — are left in place,
975
- whatever they are named. Ownership is read from the file, not the name: a wrapper is removed only when
976
- it carries the delegation line to the canonical document of the same name, or is a symlink into
977
- `.qfai/assistant/skill/`. One consequence of that: a symlink has no content to prove who wrote it, so
978
- if you publish a canonical skill of your own under a name QFAI itself once shipped, `--force` removes
979
- that link. Your `.qfai/assistant/skill/` entry is untouched; re-create the link to publish it again.
980
998
 
981
999
  ### Supported hosts
982
1000
 
@@ -986,7 +1004,9 @@ No host is declared supported in this release.
986
1004
  ### Cross-AI rules and the writing reminder
987
1005
 
988
1006
  `npx qfai init` seeds `.agents/rules/**`, the rule set every AI coding agent in
989
- the project shares, and points `AGENTS.md` and `CLAUDE.md` at it. One of those
1007
+ the project shares, and points `AGENTS.md` at it, with a line for each rule that says when
1008
+ to read it. Claude Code reads `AGENTS.md` when the project has no `CLAUDE.md` (Claude Code
1009
+ 2.1.277 or later). One of those
990
1010
  rules, `documentation-clarity.md`, sets the writing standard for pull requests,
991
1011
  issues, code comments and Markdown: no project-local identifiers, no account of
992
1012
  how the work went, cut to what a reader outside the team can follow.
@@ -4,6 +4,15 @@
4
4
  routing:
5
5
  # The triage steps change no tracked file, so each has one author and no
6
6
  # build or test phase.
7
+ - step: triage-owned-cleanup
8
+ phases:
9
+ - id: cleanup
10
+ mandatory_agents: [devops-ci-engineer]
11
+ conditional_agents: []
12
+ parallel_groups: []
13
+ rerun_policy: changed-scope-dependents
14
+ review_profile: default
15
+
7
16
  - step: triage-close
8
17
  phases:
9
18
  - id: record
@@ -0,0 +1,12 @@
1
+ route: run-owned-cleanup
2
+ family: upkeep
3
+ stages:
4
+ - id: cleanup
5
+ kind: triage
6
+ steps: [triage-owned-cleanup]
7
+ - id: close
8
+ kind: triage
9
+ steps: [triage-close]
10
+ after: [cleanup]
11
+ decisionPoints: []
12
+ branchPoints: []
@@ -30,7 +30,7 @@
30
30
  "command": "node",
31
31
  "args": [
32
32
  "-e",
33
- "try{let p;try{p=JSON.parse(require('fs').readFileSync(0,'utf8')).prompt}catch{}if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
33
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
34
34
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
35
35
  "structured-question"
36
36
  ]
@@ -46,7 +46,7 @@
46
46
  "command": "node",
47
47
  "args": [
48
48
  "-e",
49
- "try{let p;try{p=JSON.parse(require('fs').readFileSync(0,'utf8')).prompt}catch{}if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
49
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
50
50
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
51
51
  "free-text-entry"
52
52
  ]
@@ -96,7 +96,7 @@
96
96
  "command": "node",
97
97
  "args": [
98
98
  "-e",
99
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
99
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}catch{}",
100
100
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
101
101
  "grilling-design-artifact"
102
102
  ]
@@ -112,7 +112,7 @@
112
112
  "command": "node",
113
113
  "args": [
114
114
  "-e",
115
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
115
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}catch{}",
116
116
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
117
117
  "grilling-delegation"
118
118
  ]
@@ -128,7 +128,7 @@
128
128
  "command": "node",
129
129
  "args": [
130
130
  "-e",
131
- "try{const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}catch{}",
131
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}catch{}",
132
132
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
133
133
  "grilling-plan"
134
134
  ]
@@ -144,7 +144,7 @@
144
144
  "command": "node",
145
145
  "args": [
146
146
  "-e",
147
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}const c=i.tool_input&&i.tool_input.command;if(typeof c==='string'&&/(^|[^\\w.-])gh([^\\w.-]|$)|api\\.github\\.com/.test(c)){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}}catch{}",
147
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const c=i.tool_input&&i.tool_input.command;if(typeof c==='string'&&/(^|[^\\w.-])gh([^\\w.-]|$)|api\\.github\\.com/.test(c)){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
148
148
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
149
149
  "api-budget"
150
150
  ]
@@ -163,7 +163,7 @@
163
163
  "command": "node",
164
164
  "args": [
165
165
  "-e",
166
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
166
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}catch{}",
167
167
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
168
168
  "documentation-clarity-after-write"
169
169
  ]
@@ -175,7 +175,7 @@
175
175
  "command": "node",
176
176
  "args": [
177
177
  "-e",
178
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}catch{}",
178
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}catch{}",
179
179
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
180
180
  "documentation-clarity-after-edit"
181
181
  ]
@@ -191,13 +191,31 @@
191
191
  "command": "node",
192
192
  "args": [
193
193
  "-e",
194
- "try{const f=require('fs'),p=require('path'),R=p.dirname(p.dirname(p.dirname(process.argv[1])));let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}const u=i.tool_input&&i.tool_input.file_path;let src=true;if(typeof u==='string'&&(x=>{const s=x.split(p.sep),b=s.pop();return p.isAbsolute(x)||s[0]==='..'||s.some(y=>['tmp','test','tests','__tests__','node_modules','.qfai','.agents'].includes(y))||b[0]==='.'||/\\.(test|spec)\\./.test(b)||['.md','.mdx','.markdown','.txt','.rst','.yml','.yaml','.json','.jsonc','.json5','.toml','.ini','.csv','.tsv','.lock','.svg','.png','.jpg','.jpeg','.gif'].includes(p.extname(b).toLowerCase())})(p.relative(R,p.resolve(R,u))))src=false;if(src){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if((n-1)%20===0){const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}}}catch{}",
194
+ "try{const f=require('fs'),p=require('path'),R=p.dirname(p.dirname(p.dirname(process.argv[1])));let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{}const u=i.tool_input&&i.tool_input.file_path;let src=true;if(typeof u==='string'&&(x=>{const s=x.split(p.sep),b=s.pop();return p.isAbsolute(x)||s[0]==='..'||s.some(y=>['tmp','test','tests','__tests__','node_modules','.qfai','.agents'].includes(y))||b[0]==='.'||/\\.(test|spec)\\./.test(b)||['.md','.mdx','.markdown','.txt','.rst','.yml','.yaml','.json','.jsonc','.json5','.toml','.ini','.csv','.tsv','.lock','.svg','.png','.jpg','.jpeg','.gif'].includes(p.extname(b).toLowerCase())})(p.relative(R,p.resolve(R,u))))src=false;if(src){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
195
195
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
196
196
  "minimal-implementation"
197
197
  ]
198
198
  }
199
199
  ]
200
200
  }
201
+ ],
202
+ "Stop": [
203
+ {
204
+ "matcher": "*",
205
+ "hooks": [
206
+ {
207
+ "type": "command",
208
+ "statusMessage": "QFAI session feedback reminder",
209
+ "command": "node",
210
+ "args": [
211
+ "-e",
212
+ "try{const f=require('fs');let i;try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{process.exit(0)}const l=typeof i.last_assistant_message==='string'?i.last_assistant_message.trim():'';if(i.stop_hook_active===true||(l.length>0&&('?'+String.fromCharCode(65311)).includes(l.slice(-1))))process.exit(0);const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}catch{}",
213
+ "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
214
+ "session-feedback"
215
+ ]
216
+ }
217
+ ]
218
+ }
201
219
  ]
202
220
  }
203
221
  }
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "type": "command",
8
8
  "statusMessage": "QFAI structured-question reminder",
9
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}catch{}\" structured-question",
9
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}\" structured-question",
10
10
  "timeout": 10
11
11
  }
12
12
  ]
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "type": "command",
18
18
  "statusMessage": "QFAI free-text entry reminder",
19
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}catch{}\" free-text-entry",
19
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}\" free-text-entry",
20
20
  "timeout": 10
21
21
  }
22
22
  ]
@@ -50,7 +50,7 @@
50
50
  {
51
51
  "type": "command",
52
52
  "statusMessage": "QFAI grilling reminder: design artifact",
53
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}catch{}\" grilling-design-artifact",
53
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}\" grilling-design-artifact",
54
54
  "timeout": 10
55
55
  }
56
56
  ]
@@ -61,7 +61,7 @@
61
61
  {
62
62
  "type": "command",
63
63
  "statusMessage": "QFAI grilling reminder: delegation",
64
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}catch{}\" grilling-delegation",
64
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}\" grilling-delegation",
65
65
  "timeout": 10
66
66
  }
67
67
  ]
@@ -72,7 +72,7 @@
72
72
  {
73
73
  "type": "command",
74
74
  "statusMessage": "QFAI api-budget reminder",
75
- "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/[^\\w.-]gh[^\\w.-]|api\\.github\\.com/.test(' '+i+' ')){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}}catch{}\" api-budget",
75
+ "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/[^\\w.-]gh[^\\w.-]|api\\.github\\.com/.test(' '+i+' ')){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}}catch{}\" api-budget",
76
76
  "timeout": 10
77
77
  }
78
78
  ]
@@ -85,13 +85,13 @@
85
85
  {
86
86
  "type": "command",
87
87
  "statusMessage": "QFAI documentation-clarity reminder",
88
- "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/^\\*\\*\\* Add File: .*\\.md\\r?\\n/m.test(i)){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}}catch{}\" documentation-clarity-after-write",
88
+ "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/^\\*\\*\\* Add File: .*\\.md\\r?\\n/m.test(i)){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}}catch{}\" documentation-clarity-after-write",
89
89
  "timeout": 10
90
90
  },
91
91
  {
92
92
  "type": "command",
93
93
  "statusMessage": "QFAI documentation-clarity reminder",
94
- "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/^\\*\\*\\* Update File: .*\\.md\\r?\\n/m.test(i)){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}}catch{}\" documentation-clarity-after-edit",
94
+ "command": "node -e \"try{const j=JSON.parse(require('fs').readFileSync(0,'utf8')),i=j.tool_input.command;if(typeof i==='string'&&/^\\*\\*\\* Update File: .*\\.md\\r?\\n/m.test(i)){const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}}catch{}\" documentation-clarity-after-edit",
95
95
  "timeout": 10
96
96
  }
97
97
  ]
@@ -102,7 +102,19 @@
102
102
  {
103
103
  "type": "command",
104
104
  "statusMessage": "QFAI minimal-implementation reminder",
105
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let go=true,j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))}catch{}try{const R=p.dirname(p.dirname(p.dirname(r))),n=(x=>{const s=x.split(p.sep),b=s.pop();return p.isAbsolute(x)||s[0]==='..'||s.some(y=>['tmp','test','tests','__tests__','node_modules','.qfai','.agents'].includes(y))||b[0]==='.'||/\\.(test|spec)\\./.test(b)||['.md','.mdx','.markdown','.txt','.rst','.yml','.yaml','.json','.jsonc','.json5','.toml','.ini','.csv','.tsv','.lock','.svg','.png','.jpg','.jpeg','.gif'].includes(p.extname(b).toLowerCase())}),a=[...j.tool_input.command.matchAll(/^\\*\\*\\* (?:(?:Add|Update|Delete) File|Move to): (.+)/gm)].map(m=>p.relative(R,p.resolve(process.cwd(),m[1].trim())));if(a.length>0&&a.every(n))go=false}catch{}if(go){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}if(Number.isInteger((n-1)/20)){const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}}}catch{}\" minimal-implementation",
105
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let go=true,j={};try{j=JSON.parse(f.readFileSync(0,'utf8'))}catch{}try{const R=p.dirname(p.dirname(p.dirname(r))),n=(x=>{const s=x.split(p.sep),b=s.pop();return p.isAbsolute(x)||s[0]==='..'||s.some(y=>['tmp','test','tests','__tests__','node_modules','.qfai','.agents'].includes(y))||b[0]==='.'||/\\.(test|spec)\\./.test(b)||['.md','.mdx','.markdown','.txt','.rst','.yml','.yaml','.json','.jsonc','.json5','.toml','.ini','.csv','.tsv','.lock','.svg','.png','.jpg','.jpeg','.gif'].includes(p.extname(b).toLowerCase())}),a=[...j.tool_input.command.matchAll(/^\\*\\*\\* (?:(?:Add|Update|Delete) File|Move to): (.+)/gm)].map(m=>p.relative(R,p.resolve(process.cwd(),m[1].trim())));if(a.length>0&&a.every(n))go=false}catch{}if(go){let n=1;if(typeof j.session_id==='string'){try{const t=p.join(require('os').tmpdir(),['qfai-reminder',j.session_id,j.agent_id||'main',process.argv[1]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];if(e===undefined){}else{const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}}catch{}\" minimal-implementation",
106
+ "timeout": 10
107
+ }
108
+ ]
109
+ }
110
+ ],
111
+ "Stop": [
112
+ {
113
+ "hooks": [
114
+ {
115
+ "type": "command",
116
+ "statusMessage": "QFAI session feedback reminder",
117
+ "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let i;try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{process.exit(0)}const l=typeof i.last_assistant_message==='string'?i.last_assistant_message.trim():'';if(i.stop_hook_active===true||(l.length>0&&('?'+String.fromCharCode(65311)).includes(l.slice(-1))))process.exit(0);const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}catch{}\" session-feedback",
106
118
  "timeout": 10
107
119
  }
108
120
  ]
@@ -45,6 +45,11 @@ specialization_tags:
45
45
  a deliberate shortcut with its ceiling and the condition that lifts it.
46
46
  - Apply `.qfai/assistant/rule/ui-procurement.md`: install a component before writing one, and for anything the prototype does not show, take the adopted system's default rather than inventing one.
47
47
  - Apply `.agents/rules/interface-clarity.md`: label a control in the user's words, never the parameter's. The label stays; the sentence explaining it goes.
48
+ - Read each screen's `supplements` and `structure` in its UI contract before adding any text.
49
+ Show the title, the group headings, the labels and the declared supplements, and no other text.
50
+ A text the screen needs that the contract lacks, or a declared one the screen should not show,
51
+ is a change to the contract and goes back to `/qfai-sdd`.
52
+ Element ids stay the same when only wording changes.
48
53
 
49
54
  ## Inputs you must read
50
55
 
@@ -51,6 +51,10 @@ specialization_tags:
51
51
  - Apply `.agents/rules/minimal-implementation.md`: the first rung is this stage's — whether the thing needs to exist. After a spec row is agreed, that question is a Change Request.
52
52
  - Apply `.qfai/assistant/rule/ui-procurement.md`: the theme is adopted from a published one, and a screen's composition comes from a catalogue before it is drawn.
53
53
  - Apply `.agents/rules/interface-clarity.md`: a screen that needs explaining is a screen that needs redesigning. Hint text only where a need was demonstrated and the control was improved first.
54
+ - Record the decisions that rule leads to in the screen contract: the `supplements` a screen shows,
55
+ each with the reason it is needed, and the `structure` of any heading beyond the title
56
+ (`.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md`).
57
+ Fix the control before adding a supplement for it.
54
58
 
55
59
  ## Inputs you must read
56
60
 
@@ -61,6 +61,13 @@ specialization_tags:
61
61
  - Require more work only on what `.qfai/assistant/rule/shared-skill-delegation-baseline.md#what-a-reviewer-may-demand-more-of-must` admits, and report any other gap as advisory.
62
62
  - Apply `.qfai/assistant/rule/ui-procurement.md`: report a region that was drawn where a catalogue had it, and name the item that would have served.
63
63
  - Apply `.agents/rules/interface-clarity.md`: report copy that explains the interface as a finding against the control under it, and name the control to fix.
64
+ - Compare the rendered text with each screen's `supplements` and `structure` in its UI contract.
65
+ A text no contract key declares is excess; a declared supplement that is not shown is an unmet need.
66
+ Name the screen, the task or state, the duplicated meaning or unmet need, and the text to remove or the control or structure to repair.
67
+ Apply `.qfai/assistant/skill/qfai-sdd/references/design-anti-patterns.md#copy-that-adds-no-meaning`.
68
+ Never cut a condition, cost, destination, result, recovery step, consequence, label, accessible name, role or state,
69
+ and never move one into a tooltip or placeholder.
70
+ Word counts are observations, never a verdict.
64
71
 
65
72
  ## Inputs you must read
66
73
 
@@ -64,6 +64,14 @@ specialization_tags:
64
64
  `/qfai-prototyping` at the validate lane, under `QFAI-AUD-001`. The shipped
65
65
  `ui-contract.sample.yaml` carries filled entries; replace them with the
66
66
  screen's own rather than emptying the list.
67
+ - For UI-bearing surfaces: every screen also declares `supplements` (`[]` when it
68
+ shows no text beyond its title, group headings and labels), and `structure` when
69
+ it shows a heading beyond its title. Check each proposed text element against the
70
+ screen's tasks and the information the user needs there, and write the ones that
71
+ stay into the contract with the reason. A text that only repeats a heading, an
72
+ action or a condition the user already has is left out. Discussion sidecars
73
+ inform these keys; they do not replace them. See
74
+ `.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md`.
67
75
 
68
76
  ## Stop conditions
69
77
 
@@ -45,14 +45,24 @@ Applies to every text an agent produces: pull request and issue titles and
45
45
  bodies, source-code comments, and Markdown files — inside the current change
46
46
  only, never beyond its diff.
47
47
 
48
- - No issue or pull-request numbers, ticket identifiers, or names only this
49
- project understands, anywhere in source code or Markdown files. Those belong
50
- in the pull request, the commit message and the changelog.
51
- - No account of how the work went. State the current behaviour and why it is
52
- that way; the history is already in the git log and the pull request.
53
- - Cut what is self-evident, repeated, or wordy.
54
- - Ordinary vocabulary, one claim per sentence, lines short enough to scan.
55
- - Bullet lists and tables for parallel items.
48
+ - Keep out issue or pull-request numbers, ticket identifiers and names only
49
+ this project understands, anywhere in source code or Markdown files. Those
50
+ belong in the pull request, the commit message and the changelog.
51
+ Pull request and issue bodies may contain numbers and links.
52
+ - Keep out any account of how the work went. State the current behaviour and
53
+ why it is that way; the history is already in the git log and the pull
54
+ request.
55
+ - Settle the message, then open with the conclusion: the title or opening says
56
+ the result, with no preamble.
57
+ - Cut what is self-evident, repeated or wordy, and announcements, recaps and
58
+ empty intensifiers.
59
+ - Use ordinary vocabulary, one claim per sentence, and name who acts; software
60
+ does not know or decide.
61
+ - Mark what is unverified, and say what a claim rests on.
62
+ - Use bullet lists and tables for parallel items, with lines short enough to
63
+ scan.
64
+ - Open a report with the outcome, and separate what was checked from what was
65
+ not.
56
66
  - Re-read every changed line afterwards. Text that reads as a literal
57
67
  translation is a defect in any language pair — rewrite it the way someone
58
68
  writing natively in that language would.
@@ -11,7 +11,18 @@ open-questions.md, and every file under paths.contractsDir. Production code and
11
11
  obligation may change in a later task, but the change must preserve or deliberately reapprove that obligation.
12
12
  QFAI-owned files under .qfai/assistant/rule/ are updated through the package, then synchronized by init.
13
13
 
14
- A new change-request row in decisions.md is permitted without an earlier change request. Existing rows are append-only records: a prior row may change its Status cell, while its ID, Content, and Approach stay fixed. A changed obligation needs a new row, even when an older row discusses the same topic.
14
+ A new change-request row in decisions.md is permitted without an earlier change request.
15
+ Existing rows are append-only records: a prior row may change its Status cell,
16
+ while its ID, Content, and Approach stay fixed. The sole ID-change exception is
17
+ explicit local renumbering of a unique branch-added decision absent from the
18
+ unique common ancestor of HEAD and a required local base. That operation changes
19
+ only its ID and provably branch-added exact references. Inherited row cells and
20
+ references stay fixed. A changed obligation needs a new row, even when an older
21
+ row discusses the same topic.
22
+
23
+ The sole Approach-change exception is the published decision prefix recovery
24
+ below. It preserves the target's ID, Content, Status and complete original
25
+ Approach suffix. It excludes inherited rows and open-question rows.
15
26
 
16
27
  ## Allowed exceptions (minimal whitelist)
17
28
 
@@ -25,6 +36,51 @@ diff changes no file under `01_policy/`, `02_business-flow/` or
25
36
  paths.contractsDir appends no `Change request:` row. A row naming a story file
26
37
  the bugfix did not touch would state an upstream change that did not happen.
27
38
 
39
+ ## Published decision prefix recovery
40
+
41
+ The SDD owner may prepend only an approved Evidence/Grounds prefix to a
42
+ published malformed decision added by the branch. Approval of this protocol
43
+ does not authorize any target. First obtain a separate new in-force recovery
44
+ change request naming the target and its repository-relative file.
45
+
46
+ 1. Fix the explicit base commit and its unique common ancestor with current
47
+ HEAD. The target ID must be absent from both. The documented published
48
+ source commit must be an ancestor of current HEAD and expose the unique
49
+ original row. Inherited records and open-question rows are excluded.
50
+ 2. Read the original raw cell bytes. The row must be single-line valid UTF-8,
51
+ with exactly one ASCII padding space at each Approach delimiter and no
52
+ additional boundary whitespace. Hash the stored Approach bytes with
53
+ SHA-256, excluding only the delimiter pipes and those two padding spaces.
54
+ Do not decode escapes, trim text or normalize it. Ambiguous boundaries,
55
+ additional padding or invalid UTF-8 stop recovery.
56
+ 3. Confirm the original Residual risk and Rollback items already satisfy the
57
+ required ordered, nonempty form. The new Approach must be exactly
58
+ `- Evidence: <actual file references> - Grounds: `, including its trailing
59
+ space, followed by the complete original Approach as a byte-identical
60
+ suffix. Preserve the original wording, order, facts, authority and scope.
61
+ The prefixed row must satisfy the existing form checks.
62
+ 4. Record the target ID, published source commit, fixed base and common
63
+ ancestor, old and new Approach digests, exact prefix, actual file evidence,
64
+ approval authority, recovery reason, residual risk and rollback in the new
65
+ request. The old digest must match the published source Approach; the new
66
+ digest must match the exact prefix plus that source value. Record actual
67
+ authority; do not invent a user answer or approval.
68
+ 5. Immediately before writing, recheck the unique current target, its
69
+ byte-identical ID, Content and Status, and its recorded old Approach digest.
70
+ Unknown or unavailable provenance, inherited or duplicate targets, stale
71
+ data or a change beyond the approved prefix stops recovery before writing.
72
+ Apply only that prefix. Keep every other row byte-identical and run the existing
73
+ validation without an exemption.
74
+ 6. Rollback removes only the recorded prefix when the current new Approach
75
+ digest still matches. Otherwise preserve the current row and stop. Removal
76
+ restores the original Approach and its validation failure. Do not reset,
77
+ amend or force push.
78
+
79
+ Status changes, supersession and a diagnostic's rewrite suggestion supply no
80
+ recovery authority and clear no form error. Inherited malformed records need a
81
+ separate owning change request; this operation cannot repair them. These are
82
+ owner instructions, not an automatic recovery command or a concurrency guarantee.
83
+
28
84
  ## Drift classes
29
85
 
30
86
  Intent drift means an approved obligation should change. Record the current obligation, the proposed behavior, realistic
@@ -112,6 +168,6 @@ copy of a locked file before deciding its lock is wrong.
112
168
  ## Non-negotiable constraints
113
169
 
114
170
  - Downstream stages do not patch protected upstream artifacts before an in-force Change request: decision row authorizes the path. The drift profile compares the branch against baseBranch and reports QFAI-DRIFT-001 for an unapproved protected change. The SDD owner may create the request row itself without a prior row.
115
- - Existing decisions.md and open-questions.md rows retain their ID, Content, and Approach. A former row may change Status; new content is appended as a new row.
171
+ - Existing decisions.md and open-questions.md rows retain their ID, Content, and Approach, except for the explicit branch-added decision renumbering and published decision prefix recovery above. Inherited rows retain those cells without exception. A former row may change Status; new content is appended as a new row.
116
172
  - Vendored assistant rules are changed in the package and synchronized into projects by `npx qfai init --force`, which overwrites a local edit. A project rule lives in a `*.local.md` overlay beside the vendored one.
117
173
  - When approval is unavailable, keep the affected items stopped and report the decision needed. Continue unrelated work.