qfai 2.1.0 → 2.1.1

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 (124) hide show
  1. package/README.md +272 -20
  2. package/assets/defaults/workflows/change-tooling.yml +0 -4
  3. package/assets/init/.claude/settings.json +8 -8
  4. package/assets/init/.codex/hooks.json +6 -6
  5. package/assets/init/.qfai/assistant/agent/implementation-reviewer.md +14 -1
  6. package/assets/init/.qfai/assistant/agent/qa-gatekeeper.md +29 -0
  7. package/assets/init/.qfai/assistant/rule/constitution.md +5 -4
  8. package/assets/init/.qfai/assistant/rule/drift-protocol.md +14 -3
  9. package/assets/init/.qfai/assistant/rule/references/asset-authoring.md +70 -0
  10. package/assets/init/.qfai/assistant/rule/references/context-summary.md +30 -0
  11. package/assets/init/.qfai/assistant/rule/references/gate-failure-autorepair.md +42 -0
  12. package/assets/init/.qfai/assistant/rule/references/griller-recommendations.md +23 -0
  13. package/assets/init/.qfai/assistant/rule/references/host-backstops.md +60 -0
  14. package/assets/init/.qfai/assistant/rule/references/placeholder-scan.md +25 -0
  15. package/assets/init/.qfai/assistant/rule/references/reviewer-findings.md +41 -0
  16. package/assets/init/.qfai/assistant/rule/references/reviewer-remit.md +35 -0
  17. package/assets/init/.qfai/assistant/rule/shared-skill-delegation-baseline.md +26 -139
  18. package/assets/init/.qfai/assistant/rule/shared-skill-operating-baseline.md +6 -137
  19. package/assets/init/.qfai/assistant/rule/stage-cost.md +1 -1
  20. package/assets/init/.qfai/assistant/rule/test-layers.md +42 -0
  21. package/assets/init/.qfai/assistant/skill/qfai-configure/SKILL.md +8 -42
  22. package/assets/init/.qfai/assistant/skill/qfai-configure/references/principles.md +25 -0
  23. package/assets/init/.qfai/assistant/skill/qfai-discussion/SKILL.md +1 -1
  24. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/discussion-completion-matrix.md +9 -2
  25. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux/platform_baselines.md +11 -0
  26. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux/review_audit_playbook.md +11 -0
  27. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux_best_practices.md +12 -36
  28. package/assets/init/.qfai/assistant/skill/qfai-grill/SKILL.md +1 -1
  29. package/assets/init/.qfai/assistant/skill/qfai-grilling/SKILL.md +16 -88
  30. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/delegated-session.md +58 -0
  31. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/session-record.md +42 -0
  32. package/assets/init/.qfai/assistant/skill/qfai-implement/SKILL.md +8 -1
  33. package/assets/init/.qfai/assistant/skill/qfai-implement/references/credential-reuse.md +9 -0
  34. package/assets/init/.qfai/assistant/skill/qfai-implement/references/cross-spec-ownership.md +41 -0
  35. package/assets/init/.qfai/assistant/skill/qfai-implement/references/oracle-strength.md +6 -1
  36. package/assets/init/.qfai/assistant/skill/qfai-implement/references/red-admissibility.md +1 -1
  37. package/assets/init/.qfai/assistant/skill/qfai-implement/references/ui-affecting.md +1 -3
  38. package/assets/init/.qfai/assistant/skill/qfai-maintain/SKILL.md +1 -1
  39. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/SKILL.md +4 -4
  40. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-guide.md +19 -5
  41. package/assets/init/.qfai/assistant/skill/qfai-prototyping/SKILL.md +1 -1
  42. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/design-md-spec.md +14 -5
  43. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/generator-prompt.md +10 -0
  44. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/handoff.md +1 -1
  45. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/iteration-loop.md +3 -4
  46. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/review-payload-schema.md +10 -2
  47. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/reviewer-prompt.md +16 -3
  48. package/assets/init/.qfai/assistant/skill/qfai-run/SKILL.md +6 -21
  49. package/assets/init/.qfai/assistant/skill/qfai-run/references/extraction.md +30 -10
  50. package/assets/init/.qfai/assistant/skill/qfai-run/references/operator-screens.md +34 -0
  51. package/assets/init/.qfai/assistant/skill/qfai-run/references/plan.md +28 -5
  52. package/assets/init/.qfai/assistant/skill/qfai-run/references/stage-points.md +29 -0
  53. package/assets/init/.qfai/assistant/skill/qfai-sdd/SKILL.md +1 -1
  54. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/concrete-abstract-cycle.md +10 -1
  55. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/contract-artifact-rules.md +21 -9
  56. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-anti-patterns.md +19 -2
  57. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-authoring.md +11 -5
  58. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-brand-catalog.md +17 -2
  59. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/requirements-decomposition.md +2 -2
  60. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-execution-playbook.md +5 -5
  61. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-phase-checklists.md +4 -4
  62. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-quality-gate.md +2 -2
  63. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-triage.md +47 -5
  64. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/spec-traceability-rules.md +15 -3
  65. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md +16 -1
  66. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-design-contract-normalization.md +1 -2
  67. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/change-request.md +2 -2
  68. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/contracts/db-contract.sample.sql +2 -2
  69. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/03_contract/cli/cli-NNNN-title.md +2 -2
  70. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/03_contract/tech.md +1 -1
  71. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/decisions.md +4 -0
  72. package/assets/init/.qfai/assistant/skill/qfai-triage/SKILL.md +1 -1
  73. package/assets/init/.qfai/assistant/skill/qfai-verify/SKILL.md +1 -1
  74. package/assets/init/.qfai/assistant/skill/qfai-verify/references/verify-output-contract.md +22 -8
  75. package/assets/init/.qfai/assistant/skill/web-research/SKILL.md +2 -10
  76. package/assets/init/.qfai/assistant/step/common-design-md/STEP.md +17 -3
  77. package/assets/init/.qfai/assistant/step/common-gate-run/STEP.md +30 -7
  78. package/assets/init/.qfai/assistant/step/discussion-uiux/STEP.md +11 -1
  79. package/assets/init/.qfai/assistant/step/implement-dep-bump/STEP.md +7 -0
  80. package/assets/init/.qfai/assistant/step/implement-scaffold/STEP.md +3 -3
  81. package/assets/init/.qfai/assistant/step/implement-seam/STEP.md +3 -1
  82. package/assets/init/.qfai/assistant/step/implement-tdd/STEP.md +3 -1
  83. package/assets/init/.qfai/assistant/step/implement-test-fix/STEP.md +2 -0
  84. package/assets/init/.qfai/assistant/step/sdd-flow/STEP.md +2 -0
  85. package/assets/init/.qfai/assistant/step/sdd-gate/STEP.md +4 -0
  86. package/assets/init/.qfai/assistant/step/sdd-triage/STEP.md +7 -3
  87. package/assets/init/.qfai/assistant/step/verify-change-note/STEP.md +8 -5
  88. package/assets/init/.qfai/assistant/step/verify-commit/STEP.md +6 -1
  89. package/assets/init/.qfai/assistant/step/verify-qfai-gate/STEP.md +20 -3
  90. package/assets/init/.qfai/assistant/step/verify-repeat-run/STEP.md +1 -1
  91. package/assets/init/.qfai/assistant/step/verify-repo-gate/STEP.md +23 -6
  92. package/assets/init/.qfai/waivers.yml +1 -1
  93. package/assets/init/root/.agents/rules/ai-readable-markdown.md +98 -0
  94. package/assets/init/root/.agents/rules/api-budget.md +4 -5
  95. package/assets/init/root/.agents/rules/documentation-clarity.md +30 -2
  96. package/assets/init/root/.agents/rules/grilling.md +4 -4
  97. package/assets/init/root/.agents/rules/interface-clarity.md +5 -5
  98. package/assets/init/root/.agents/rules/minimal-implementation.md +8 -0
  99. package/assets/init/root/.agents/rules/reminders.json +2 -2
  100. package/assets/init/root/.agents/rules/temporary-files.md +14 -5
  101. package/assets/init/root/.agents/rules/untrusted-content.md +3 -3
  102. package/assets/init/root/.agents/rules/user-questions.md +34 -7
  103. package/assets/init/root/.gitattributes +9 -0
  104. package/assets/init/root/.github/workflows/qfai-docs.yml +43 -11
  105. package/assets/init/root/.github/workflows/qfai-tests.yml +58 -15
  106. package/assets/init/root/.github/workflows/qfai-validate.yml +49 -42
  107. package/assets/init/root/AGENTS.md +1 -0
  108. package/assets/init/root/CLAUDE.md +1 -0
  109. package/assets/mdschema/story/03_contract/tech.mdschema.yml +7 -3
  110. package/assets/sandbox-templates/default-deny.yaml +0 -2
  111. package/assets/schemas/workflow/plan.schema.json +9 -2
  112. package/assets/scripts/check-mdschema.mjs +172 -5
  113. package/dist/cli/index.cjs +3979 -3055
  114. package/dist/cli/index.cjs.map +1 -1
  115. package/dist/cli/index.mjs +3167 -2243
  116. package/dist/cli/index.mjs.map +1 -1
  117. package/dist/index.cjs +3024 -2500
  118. package/dist/index.cjs.map +1 -1
  119. package/dist/index.d.cts +23 -9
  120. package/dist/index.d.ts +23 -9
  121. package/dist/index.mjs +2881 -2358
  122. package/dist/index.mjs.map +1 -1
  123. package/docs/MIGRATION-2.0.0.md +1 -1
  124. package/package.json +3 -3
package/README.md CHANGED
@@ -92,7 +92,7 @@ See [Invoking a stage directly](#invoking-a-stage-directly-expert-path).
92
92
 
93
93
  You state the change once, in your own words.
94
94
  The `qfai-run` skill reads it into facts: what it asks for, its risks, and how sure that reading is.
95
- `npx qfai workflow plan` then picks one of 35 fixed routes from those facts by ordered decision rules
95
+ `npx qfai workflow plan` then picks one of the fixed routes from those facts by ordered decision rules
96
96
  and returns the route's plan, writing nothing. The session runs each step of the plan in order and
97
97
  asks you only the decisions that are yours and each release. You type no stage name.
98
98
 
@@ -325,7 +325,7 @@ flowchart LR
325
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
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
327
 
328
- - `D-DEPRECATED-PATH` means the legacy assistant layout remains past its
328
+ - `QFAI-DEPRECATED-001` means the legacy assistant layout remains past its
329
329
  supported window. Run `npx qfai init --upgrade-assistant-tree` to copy
330
330
  recognized files into the current tree without deleting their sources.
331
331
 
@@ -403,6 +403,8 @@ paths:
403
403
  testsDir: tests
404
404
  validation:
405
405
  failOn: error # error | warning | never
406
+ staleTerms: # terms the specs must no longer state, such as a rejected option
407
+ - SMTP
406
408
  traceability:
407
409
  testFileGlobs:
408
410
  - "src/**/*.test.ts"
@@ -416,6 +418,7 @@ uiux:
416
418
 
417
419
  Notes.
418
420
 
421
+ - `validation.staleTerms` lists terms a project no longer wants in its specs. `qfai validate` warns on each line of a spec document that states one, apart from `decisions.md` and `open-questions.md`.
419
422
  - `validate.json` is a **public** surface: its keys are documented in
420
423
  `.qfai/assistant/skill/qfai-verify/references/validate-json-schema.md`.
421
424
  Changes to its keys follow the `@api` classification in
@@ -464,10 +467,16 @@ there, and all three open with a ``# Generated by `qfai init` `` line. All three
464
467
  every push to `main` or `master` and on every pull request, and all three run on the
465
468
  runner `vars.QFAI_CI_RUNNER` names (`ubuntu-latest` when you set nothing).
466
469
 
470
+ `npx qfai init` writes a workflow again whenever its file is missing. To stop it
471
+ writing one, set `workflow.skipShipped` in `qfai.config.yaml` to a list of file
472
+ names, for example `[qfai-tests.yml]`, and delete the file.
473
+
467
474
  - `qfai-validate.yml` runs `npx qfai validate --profile full --fail-on error`,
468
- and on a pull request also `npx qfai validate --profile drift --fail-on error`.
469
- The `full` profile evaluates every gate group except drift, so without the
470
- second run nothing there could fail on a downstream edit to upstream SSOT.
475
+ and on a pull request also `npx qfai validate --profile drift --fail-on error`,
476
+ as a second step of the same job. The `full` profile evaluates every gate group
477
+ except drift, so without the second run nothing there could fail on a downstream
478
+ edit to upstream SSOT. The second step runs after a failing first one, so one run
479
+ reports both profiles, and the dependencies are installed once.
471
480
  It installs dependencies from whichever lockfile the repository has (pnpm /
472
481
  yarn / npm) and falls back to `npm install` when there is none, and it takes
473
482
  the Node version from your `.nvmrc` or `.node-version`, warning and
@@ -477,9 +486,10 @@ runner `vars.QFAI_CI_RUNNER` names (`ubuntu-latest` when you set nothing).
477
486
  holding a `pnpm-lock.yaml` with no such field fails the job with an
478
487
  annotation naming the field rather than reporting a validation it never ran.
479
488
  Declare `"packageManager": "pnpm@X.Y.Z"` so CI matches the version you
480
- develop against. The `full` profile includes the `QFAI-TEST-001` test-todo
481
- stub gate, so the job can fail your default branch on findings your existing
482
- CI never checked.
489
+ develop against. The `full` profile includes the test stub gate
490
+ (`QFAI-TEST-001` for a todo, `QFAI-TEST-003` for a skip), so the job can fail
491
+ your default branch on findings your existing CI never checked. See
492
+ "Skipped and todo tests" below for what the gate reports and what it cannot see.
483
493
  - `qfai-tests.yml` declares one lane per test layer (unit, component,
484
494
  integration, api, e2e) and runs none of them until you opt in: a lane runs
485
495
  only when your `package.json` declares the matching `test:<layer>` script
@@ -506,7 +516,32 @@ A push to `main` or `master` runs the test lanes and the document checks again b
506
516
  default, because nothing in the files can tell whether that commit passed a pull
507
517
  request first. If your branch protection requires these checks before every merge,
508
518
  set the repository variable `QFAI_CI_PUSH_POLICY` to `protected`: the push then runs
509
- neither, and `qfai validate` still runs as the post-merge check.
519
+ neither, and `qfai validate` still runs as the post-merge check. If you run
520
+ `qfai validate` in a post-merge job of your own, set it to `none` instead: the
521
+ push then starts no job of the validate workflow either.
522
+
523
+ `protected` is safe only when GitHub requires the branch to be up to date before
524
+ a pull request can merge: the branch protection option "Require branches to be up
525
+ to date before merging", called `strict` in the API. Without it, two pull requests
526
+ can each pass on their own and break the branch once both are merged. One that
527
+ changes a function signature and one that starts calling the old signature is the
528
+ usual case. The default branch is then red while every pull request was green.
529
+
530
+ Requiring up-to-date branches makes every parallel pull request update and rerun
531
+ after each merge. If that costs too much, there is a middle option between running
532
+ the full lanes again and running nothing: keep `QFAI_CI_PUSH_POLICY` at
533
+ `protected`, and add a small post-merge job of your own to a workflow you
534
+ maintain.
535
+
536
+ - It runs only what can break when two pull requests combine: the type check, and
537
+ the schema or contract consistency checks. The shipped `qfai-validate.yml`
538
+ already runs the validator on every push, so the job does not repeat it.
539
+ - It installs dependencies once, and uses one concurrency group with
540
+ `cancel-in-progress: true`, so a newer push cancels an older run. The newest
541
+ commit contains the older ones, so nothing goes unchecked.
542
+ - It leaves out the test suites. A break that only they would catch shows up on
543
+ the next pull request, which fails on code it did not change. In one project the
544
+ job took about two billed minutes where the full lanes took about 29.
510
545
 
511
546
  All three files are copied create-only — `qfai init` writes a workflow only
512
547
  where no file of that name exists, and never overwrites one, not even with
@@ -577,6 +612,215 @@ Typical customizations.
577
612
  - Add a `doctor` step before validate if you want to fail fast on path/glob/config issues.
578
613
  - Publish `.qfai/report/validate.json`, `report.md`, and relevant `.qfai/report/run-*/` logs as CI artifacts.
579
614
 
615
+ ### What a run costs
616
+
617
+ On a private repository with GitHub-hosted runners, cost follows the number of
618
+ jobs, not how long they run. Each job rounds up to a whole minute, a skipped job
619
+ costs nothing, and a workflow that starts several quick jobs on every event costs
620
+ more than one slow job. A closed pull request starts a run too, but every job in
621
+ it declines, so nothing is billed.
622
+
623
+ Jobs the shipped workflows start on each `opened`, `synchronize` and `reopened`
624
+ event of a pull request:
625
+
626
+ | Workflow | Nothing to run | Lane runs |
627
+ | ------------------- | --------------------------------- | --------------------------------------------------- |
628
+ | `qfai-validate.yml` | 1: `qfai validate`, both profiles | 1 |
629
+ | `qfai-tests.yml` | 1: `change detection` | 2, plus one per selected lane (at most 5) |
630
+ | `qfai-docs.yml` | 1: `change scope` | 4: adds the `shape` and `mermaid` checks and `docs` |
631
+
632
+ When nothing is selected, `verdict` is skipped, and when nothing a document check
633
+ reads changed, `docs` is skipped. Each workflow then starts one job, and the
634
+ detection and scope jobs say in a notice annotation why. One pull request event
635
+ starts 3 jobs in all, and the most it can start is 12. On a push to the default
636
+ branch, `qfai-validate.yml` starts 1 job, `QFAI_CI_PUSH_POLICY=protected` brings the
637
+ other two workflows down to their idle counts, and `none` also skips the validate
638
+ job, which leaves `change detection` and `change scope`.
639
+
640
+ Four of those jobs install nothing and run no test: `change detection`, `verdict`,
641
+ `change scope` and `docs`. They take seconds, so each bills one minute.
642
+ They read `vars.QFAI_CI_LIGHT_RUNNER`, so the price of that minute is a choice.
643
+ On a private repository with GitHub-hosted runners, set it to `ubuntu-slim`:
644
+
645
+ ```bash
646
+ gh variable set QFAI_CI_LIGHT_RUNNER --body ubuntu-slim
647
+ ```
648
+
649
+ `ubuntu-slim` has 1 CPU and 5 GB of memory, runs unprivileged, limits a job to 15
650
+ minutes, and bills at a lower per-minute rate than `ubuntu-latest`; GitHub's
651
+ pricing page has the current figure. The light jobs run only Bash, Git and Node, all
652
+ of which it carries, and the workflows keep `ubuntu-latest` as the default, so this
653
+ is your variable to set. Do not point `QFAI_CI_RUNNER` at it: the jobs that install
654
+ dependencies, need Docker or can run past 15 minutes do not belong on it. A label
655
+ GitHub does not offer to your account leaves a job queued rather than failing it.
656
+
657
+ Each workflow file states its own counts in a "Jobs per pull request event" row of
658
+ its header table. `npx qfai doctor` reports the same counts for the workflows
659
+ installed in your repository, with the runner each job asks for. It is an `info`
660
+ finding and reads the files only. It reads none of your repository variables, so it
661
+ shows the variables a job falls back through and not the label they resolve to.
662
+
663
+ ### Required checks and branch protection
664
+
665
+ Require these three checks, by exactly these names:
666
+
667
+ - From `qfai-tests.yml`: `verdict`
668
+ - From `qfai-docs.yml`: `qfai docs (document shape and Mermaid syntax)`
669
+ - From `qfai-validate.yml`: `qfai validate (full profile, fail on error)`
670
+
671
+ Each one fails when the work behind it fails. `verdict` and the `qfai docs` check
672
+ are skipped when nothing was selected or nothing a document check reads changed,
673
+ and GitHub counts a skipped job as satisfying a required check. The jobs behind
674
+ them are named by their matrix value, such as `qfai tests (unit)`, and a skipped
675
+ matrix job is reported under its unexpanded name, so a required check cannot name
676
+ them.
677
+
678
+ A check name is what branch protection matches. When you rename a job, or change
679
+ what it covers, change the required check in the same step. While the two differ
680
+ the required check is never reported, and every pull request is blocked. The
681
+ validate check above is the one to watch: on a pull request it covers the `full`
682
+ and `drift` profiles, and if you change the profiles you run, its name keeps
683
+ saying "full profile".
684
+
685
+ A job that an `if:` condition skips is reported as skipped, and that satisfies a
686
+ required check. A workflow that a workflow-level `paths` or `paths-ignore` filter
687
+ keeps from starting reports nothing, so its required check stays pending and the
688
+ pull request cannot merge. Put the decision in a job, and skip jobs. The shipped
689
+ `change detection` and `change scope` jobs do that. Do not put a `paths` filter on
690
+ a workflow with a required check.
691
+
692
+ The "Required check" row of each workflow's header table names the job to require.
693
+
694
+ ### Checking workflow files
695
+
696
+ GitHub starts no job from a workflow file it cannot parse. The run shows as failed
697
+ with no jobs, on every push to every branch, and a change to the file does not show
698
+ up in its own pull request's checks, because the broken workflow never starts. A key
699
+ that appears twice in one mapping is enough, such as `env:` twice in one step after a
700
+ scripted edit, and many YAML loaders accept it without a word.
701
+
702
+ `npx qfai doctor` parses each `.yml` and `.yaml` file under `.github/workflows/` and
703
+ `.github/actions/` and warns with the file and the line. Run `actionlint` as well, and
704
+ run it after any scripted edit to a workflow. It also reports duplicate keys with line
705
+ numbers, and it checks expressions and job references that a YAML parser cannot. It
706
+ can be one step of a job you already run, such as a change-detection job, so it adds
707
+ no job.
708
+
709
+ ### Skipped and todo tests
710
+
711
+ The stub gate is a static scan of the files that
712
+ `validation.traceability.testFileGlobs` selects. It blanks comments and string
713
+ literals first, then matches text patterns. The shipped workflow runs
714
+ `--fail-on error`, and every finding below is an error.
715
+
716
+ | What the scan finds | Finding |
717
+ | -------------------------------------------------------------------------------------------------------- | --------------- |
718
+ | `it.todo`, `test.todo`, `describe.todo` | `QFAI-TEST-001` |
719
+ | `it.skip`, `test.skip`, `describe.skip`, with a modifier before or after (`.skip.each`, `.concurrent`) | `QFAI-TEST-003` |
720
+ | The other stacks' placeholders: `pytest.skip`, `t.Skip`, `@Disabled`, `@Ignore`, `#[ignore]`, `[Ignore]` | `QFAI-TEST-001` |
721
+
722
+ It cannot see a skip that is not a text pattern, and none of these is reported:
723
+
724
+ - a conditional skip: `describe.skipIf(...)`, `it.skipIf(...)` and `it.runIf(...)`
725
+ - a form that selects the skip at run time, such as
726
+ `const suite = enabled ? describe : describe.skip`
727
+ - a skip called inside a running test, such as `ctx.skip()`
728
+ - a suite that returns early when the environment is missing, such as an unset
729
+ database URL
730
+ - a test file that the globs do not select; with no globs set the scan reports
731
+ `QFAI-TEST-002` and reads nothing
732
+
733
+ A suite skipped for a missing database therefore passes CI, and so does a suite
734
+ whose database failed to start. Only the test runner knows what was skipped in a
735
+ given run. Have it write a JSON report, and fail the job when a file skips more
736
+ tests than a list of named, allowed skips permits. Jest and Vitest write the same
737
+ shape: `jest --json --outputFile=test-report.json`, or
738
+ `vitest run --reporter=json --outputFile=test-report.json`. Add the flags to
739
+ your `test:<layer>` script, save this script as `scripts/check-skipped-tests.mjs`,
740
+ and run it in a step of `qfai-tests.yml` after the step that runs the tests:
741
+
742
+ ```js
743
+ import { readFileSync } from "node:fs";
744
+ import path from "node:path";
745
+
746
+ const allowed = JSON.parse(readFileSync("allowed-skips.json", "utf8"));
747
+ const report = JSON.parse(readFileSync("test-report.json", "utf8"));
748
+ let failed = false;
749
+ for (const file of report.testResults) {
750
+ const name = path.relative(process.cwd(), file.name).split(path.sep).join("/");
751
+ const skipped = file.assertionResults.filter(
752
+ (test) => test.status !== "passed" && test.status !== "failed",
753
+ ).length;
754
+ if (skipped > (allowed[name] ?? 0)) {
755
+ console.error(`${name}: ${skipped} tests did not run, ${allowed[name] ?? 0} allowed`);
756
+ failed = true;
757
+ }
758
+ }
759
+ process.exit(failed ? 1 : 0);
760
+ ```
761
+
762
+ `allowed-skips.json` maps a test file to the number of tests it may skip, such as
763
+ `{ "orders.test.ts": 2 }`. Start from the count you measure today. The
764
+ list should only ever shrink: a pull request may remove an entry or lower a count,
765
+ and raising one is a decision a reviewer makes on purpose.
766
+
767
+ ### Writing a change-scope gate for your own workflows
768
+
769
+ If you write a gate that skips your own heavy jobs on a small change, copy the
770
+ shape of the shipped `change detection` job: it falls back to running every lane
771
+ when the base commit is missing, the clone is shallow, the base is unreachable, the
772
+ diff fails, or a changed path is outside the set it knows. The rules that keep a
773
+ gate from skipping what it should have run:
774
+
775
+ - Decide from what the runners and tests read, not from directory names.
776
+ - Fail open. When the input is unknown, run everything: a skipped check claims the
777
+ change is fine, and a check that runs costs only its minutes.
778
+ - Run the gate's own table of cases, sample paths with the decision each must
779
+ get, before the gate. A pull request that changes the gate runs the checks that
780
+ cover the gate.
781
+ - Keep job names fixed, so required checks stay satisfied.
782
+ - Decide in a job and skip jobs with `if:`. Never use a workflow-level `paths`
783
+ filter, as the section above explains.
784
+
785
+ Three mistakes this prevents:
786
+
787
+ - **Excluding a directory the runner reads.** A gate treated all of
788
+ `tests/integration/` as irrelevant to end-to-end tests, but the browser runner's
789
+ configuration runs the `*.browser.spec.ts` files in it. A pull request that fixed
790
+ one of them ran no end-to-end job and passed. Exclude only the files the runner
791
+ does not read.
792
+ - **Trusting a general plan over the repository.** "A change to tests only can
793
+ skip the production build" is wrong where the build type-checks the tests
794
+ through the compiler's `include`. Read the configuration before you write the
795
+ rule.
796
+ - **A gate that skips its own tests.** See the third rule above.
797
+
798
+ ### Measuring Actions minutes
799
+
800
+ Cost on a private repository is hard to read from the outside. The billing
801
+ endpoint for an organization needs an admin scope and may answer with an error,
802
+ the timing endpoint for a single run reports `billable` as zero, and the detailed
803
+ usage report needs an admin. This method needs only read access to the repository:
804
+
805
+ 1. List the runs of one pull request, by its head commit:
806
+ `gh api "repos/OWNER/REPO/actions/runs?head_sha=SHA" --jq '.workflow_runs[].id'`
807
+ 2. For each run, list its jobs:
808
+ `gh api "repos/OWNER/REPO/actions/runs/RUN_ID/jobs?per_page=100" --jq '.jobs[] | [.name, .conclusion, .started_at, .completed_at] | @tsv'`
809
+ 3. A job bills `max(1, ceil(seconds / 60))` minutes, where the seconds run from
810
+ `started_at` to `completed_at`. A skipped job bills zero.
811
+ 4. Compare pull requests by the same author and of the same kind of change, and
812
+ compare jobs and runs as well as minutes. Pull requests that only changed CI
813
+ settings are the case a change helps most, so they overstate the saving.
814
+ Compare pull requests that changed source and tests.
815
+ 5. Count the runs of a day with
816
+ `gh api "repos/OWNER/REPO/actions/runs?created=YYYY-MM-DD&per_page=1" --jq .total_count`.
817
+ It is one cheap call per day, and it shows days that stand out. It also follows
818
+ the amount of development, so it cannot prove a reduction by itself.
819
+
820
+ A job that never started looks different from a job that failed: it has no steps,
821
+ an empty runner name and a duration of about two seconds. Its logs are not found,
822
+ which is not the logs having expired.
823
+
580
824
  ### Keeping QFAI itself up to date
581
825
 
582
826
  A `qfai` bump is the one dependency update that is not finished when the version
@@ -590,6 +834,23 @@ Moving a project from the spec-pack layout to the story tree also requires
590
834
  `/qfai-migration-v1-to-v2`. `init --force` refreshes shipped assets; the
591
835
  migration skill moves project content and reports items that need a person.
592
836
 
837
+ To upgrade by hand:
838
+
839
+ 1. Install the new version with your package manager, for example
840
+ `npm i -D qfai@<version>`.
841
+ 2. Move every edit you made to a shipped skill, step, agent or rule into
842
+ `.qfai/assistant/skill.local/` or a rule overlay. The next step overwrites
843
+ the shipped copies.
844
+ 3. Run `npx qfai init --force`. It replaces the skills and steps an agent may
845
+ be reading, so an agent upgrading the project reads them again from disk
846
+ afterwards.
847
+ 4. Delete the files the release notes of the new version list as retired, once
848
+ nothing you keep reads them.
849
+ 5. Run `npx qfai doctor` and `npx qfai validate`, and fix what they report.
850
+ 6. Commit the result in one change.
851
+
852
+ An agent asked to upgrade `qfai` follows these steps as a dependency update.
853
+
593
854
  Merging the bump on its own leaves the repository claiming a version whose
594
855
  skills it does not have. If you use Renovate, QFAI publishes a preset that keeps
595
856
  the two together:
@@ -747,8 +1008,8 @@ Each hook runs `node` directly, with no shell and no network, and prints one
747
1008
  message from `.agents/rules/reminders.json`. A missing or unreadable message
748
1009
  file prints nothing. `qfai init` refreshes that file wherever the project has
749
1010
  not edited it, so a new release's wording reaches an existing project without
750
- changing `.claude/settings.json`. Remove the entries to turn the reminder off;
751
- the rule still applies.
1011
+ changing `.claude/settings.json`. Remove the entries from that file and from
1012
+ `.codex/hooks.json` to turn the reminder off; the rule still applies.
752
1013
 
753
1014
  Codex gets the same reminders from `.codex/hooks.json` once you trust the
754
1015
  project's hooks with `/hooks`. Each is one command line that runs the same under
@@ -764,15 +1025,6 @@ kept, and the run names it. To update one by hand, run
764
1025
  `npx qfai init --dir <scratch-dir>` in an unused scratch directory and compare
765
1026
  its `.claude/settings.json` with your project's file, group by group.
766
1027
 
767
- ## Contributing (for QFAI maintainers)
768
-
769
- This repository is a monorepo, and the distributable package is under `packages/qfai`.
770
- The repository root `README.md` and `packages/qfai/README.md` are kept aligned by
771
- `scripts/check-readme-alignment.mjs`, which CI runs as part of `pnpm ci:lint`: every line
772
- outside a `readme-align:ignore-start` / `readme-align:ignore-end` HTML-comment block must be
773
- identical in both files. When you change documentation, apply the edit to both READMEs, or
774
- wrap the intentionally file-specific part in those markers.
775
-
776
1028
  ## License
777
1029
 
778
1030
  MIT
@@ -1,13 +1,9 @@
1
1
  route: change-tooling
2
2
  family: upkeep
3
3
  stages:
4
- - id: diagnose
5
- kind: diagnose
6
- steps: [implement-diagnose]
7
4
  - id: edit
8
5
  kind: implement
9
6
  steps: [implement-tooling]
10
- after: [diagnose]
11
7
  - id: note
12
8
  kind: verify
13
9
  steps:
@@ -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>]/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{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{}",
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{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{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{}",
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 m=JSON.parse(require('fs').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{}}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{}",
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 m=JSON.parse(require('fs').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{}}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{}",
116
116
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
117
117
  "grilling-delegation"
118
118
  ]
@@ -144,7 +144,7 @@
144
144
  "command": "node",
145
145
  "args": [
146
146
  "-e",
147
- "try{const c=JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input.command;if(typeof c==='string'&&/(^|[^\\w.-])gh([^\\w.-]|$)|api\\.github\\.com/.test(c)){const m=JSON.parse(require('fs').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{}}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{}",
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 m=JSON.parse(require('fs').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{}}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{}",
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 m=JSON.parse(require('fs').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{}}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{}",
179
179
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
180
180
  "documentation-clarity-after-edit"
181
181
  ]
@@ -191,7 +191,7 @@
191
191
  "command": "node",
192
192
  "args": [
193
193
  "-e",
194
- "try{const m=JSON.parse(require('fs').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{}}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{}",
195
195
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
196
196
  "minimal-implementation"
197
197
  ]
@@ -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){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{}}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",
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){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{}}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",
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 i=JSON.parse(require('fs').readFileSync(0,'utf8')).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){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{}}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",
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 i=JSON.parse(require('fs').readFileSync(0,'utf8')).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){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{}}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",
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 i=JSON.parse(require('fs').readFileSync(0,'utf8')).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){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{}}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",
95
95
  "timeout": 10
96
96
  }
97
97
  ]
@@ -102,7 +102,7 @@
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){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{}}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",
106
106
  "timeout": 10
107
107
  }
108
108
  ]
@@ -30,8 +30,13 @@ specialization_tags:
30
30
 
31
31
  ## Domain Responsibilities
32
32
 
33
- - Review changed production code and tests against the affected BF, AC, EX, BR and contracts. Check correctness, input validation, security, performance, maintainability and operational failure paths.
33
+ - Review changed production code and tests against the affected BF, AC, EX, BR and contracts. Check correctness, performance, maintainability and operational failure paths, and security as set out below.
34
34
  - Apply the repository review checklist and `.agents/rules/minimal-implementation.md`. Require a concrete smaller implementation when reporting excess; preserve every safety-floor obligation.
35
+ - Check that the change adds test files sized like their neighbours, commits no
36
+ scratch checks, follows § 4 on unrequested fixes, including its exception for
37
+ a necessary fix and reporting requirement, and states any assumption it built
38
+ on (`.qfai/assistant/rule/test-layers.md#test-suite-sizing`,
39
+ `.agents/rules/minimal-implementation.md` § 4).
35
40
  - Check each new module against the `## Architecture` table of `<paths.contractsDir>/tech.md`: it belongs to one layer and imports only from the layers its row lists. A crossing is a finding.
36
41
  - In TypeScript, flag unjustified assertions, unchecked `unknown`, needless generic complexity and promises that callers neither await nor return.
37
42
  - Read the whole of every file the change touches for silent failure and type design, not only the lines the change adds or alters.
@@ -46,6 +51,14 @@ specialization_tags:
46
51
  - Type design, against § 2 of the same rule: a value crossing a trust boundary is parsed there into a form that cannot hold an invalid value.
47
52
  Flag mutable internals exposed to outside code, an invariant held only by documentation, validation missing at construction,
48
53
  enforcement that differs from one mutation to another, and outside code left to maintain an invariant the type should own.
54
+ - Security, read on the same scope as silent failure and type design above. Check the three shapes the drift protocol names:
55
+ missing validation on an input the code already treats as trusted, credential or personal-data exposure, and an injection or traversal path opened by the change.
56
+ Cover injection, cross-site scripting, server-side request forgery, hardcoded secrets, insecure direct object reference, auth bypass,
57
+ unsafe deserialization and path traversal. Judge validation at a trust boundary against `.agents/rules/minimal-implementation.md` § 2.
58
+ - Follow every input the change adds or alters to where it is used, across files the change did not touch.
59
+ A path the change opens there, such as an insecure direct object reference, a server-side request forgery or an auth bypass reached through several files, blocks.
60
+ This is the one reach beyond the touched files, and it covers only the inputs the change handles.
61
+ A security finding the review demonstrates traces to `defect:security`.
49
62
  - Check the EX test's oracle, selector and RED/GREEN/Refactor evidence.
50
63
  - Check for code written only to pass a test: no value hard-coded to the test's
51
64
  inputs and no branch written only for the test, and a wrong test or infeasible
@@ -59,6 +59,11 @@ scope.
59
59
  code, control, setting or copy to remove or simplify and what replaces it.
60
60
  Do not weaken an active obligation to reduce code.
61
61
  - Treat test volume and density as review signals, not independent hard gates.
62
+ - Check that the change adds test files sized like their neighbours, commits no
63
+ scratch checks, follows § 4 on unrequested fixes, including its exception for
64
+ a necessary fix and reporting requirement, and states any assumption it built
65
+ on
66
+ (`.qfai/assistant/rule/test-layers.md#test-suite-sizing`, `.agents/rules/minimal-implementation.md` § 4).
62
67
 
63
68
  ## Coverage gate
64
69
 
@@ -79,6 +84,30 @@ required gap or weak oracle; report a gap owned by another stage with that
79
84
  owner. During SDD, assess the requirement links without demanding tests that
80
85
  implementation has not written.
81
86
 
87
+ ## Refactor survival check (advisory)
88
+
89
+ Read each test on four questions. Three are already judged elsewhere; apply
90
+ them there, not twice.
91
+
92
+ | Question | Where it is judged |
93
+ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
94
+ | Does it cover the behaviour that matters, not just the lines run? | The coverage gate above |
95
+ | Are the recurring gaps covered: kept failures and boundary cases? | The coverage gate above |
96
+ | Would it fail on a concrete regression? | `skill/qfai-implement/references/oracle-strength.md`, proof per example |
97
+ | Would it survive a refactor that keeps the behaviour? | This check |
98
+
99
+ A test fails the last question when it asserts on what the contract does not
100
+ name: a private function, an internal call order, a mock of the code's own
101
+ collaborators, or the structure of a value rather than what it means. Such a
102
+ test breaks on a safe change and teaches authors to weaken the test instead of
103
+ fixing the code.
104
+
105
+ Read the whole of every test file the change touches, not only the tests it
106
+ adds or alters. Record each finding with the test and a concrete
107
+ behaviour-preserving change it would fail on. A finding with no named change is
108
+ not admitted. A finding on a test that existed before the change is recorded
109
+ and deferred. This check does not REVISE on its own and carries no score.
110
+
82
111
  ## Completion and runtime gate
83
112
 
84
113
  - Read the current BF's test selectors, command results and reviewer
@@ -95,7 +95,7 @@ The test obligation is determined by the ID and the test's layer:
95
95
 
96
96
  - `BF-*` requires a `QFAI:BF-NNNN` annotation in an E2E test.
97
97
  - `AC-*` requires a `QFAI:AC-NNNN-NNNN-NN` annotation in an integration or API test.
98
- - `EX-*` requires a `QFAI:EX-NNNN-NNNN-NN` annotation in a selected non-E2E test file.
98
+ - `EX-*` requires a `QFAI:EX-NNNN-NNNN-NN` annotation in a selected non-E2E test file, on the comment line directly before the test declaration it marks.
99
99
 
100
100
  `.qfai/assistant/rule/test-layers.md` defines the layer directories. Test
101
101
  selection uses `validation.traceability.testFileGlobs`. A missing BF, AC, or EX
@@ -429,7 +429,8 @@ This article survives context compaction because `.qfai/assistant/rule/constitut
429
429
 
430
430
  Scratch files an agent creates for its own convenience — working notes, one-off
431
431
  scripts, captured command output, downloaded samples, intermediate data —
432
- must be placed under the repository‑root `tmp/` directory.
432
+ go in the scratch directory the host names for the session, and under the
433
+ repository-root `tmp/` directory where the host names none.
433
434
 
434
435
  Scope: this article is about files written **into the working tree**. Two kinds
435
436
  of output are outside it:
@@ -445,7 +446,7 @@ of output are outside it:
445
446
  Rules:
446
447
 
447
448
  1. **Never** create a scratch file in the repository root, `src/`, `.qfai/spec/`, or any other production/artifact directory.
448
- 2. Use `tmp/` (repository root) as the sole staging area. Create subdirectories as needed (e.g., `tmp/notes/`, `tmp/capture/`).
449
+ 2. When the host names a scratch directory for the session, use it. Otherwise use `tmp/` (repository root) as the sole staging area, with subdirectories as needed (e.g., `tmp/notes/`, `tmp/capture/`). A file the repository's own tooling must read from the working tree goes under `tmp/` whichever the host names.
449
450
  3. `tmp/` MUST be listed in `.gitignore` so temporary files are never committed.
450
- 4. Clean up `tmp/` contents when the task that created them is complete.
451
+ 4. Clean up `tmp/` contents when the task that created them is complete. The host's own scratch directory is the host's to clean.
451
452
  5. If a scratch file is found outside `tmp/` **in the working tree**, treat it as a defect and move or delete it immediately. A test's `mkdtemp` sandbox and configured toolchain output are not scratch files — see Scope above.