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.
- package/README.md +272 -20
- package/assets/defaults/workflows/change-tooling.yml +0 -4
- package/assets/init/.claude/settings.json +8 -8
- package/assets/init/.codex/hooks.json +6 -6
- package/assets/init/.qfai/assistant/agent/implementation-reviewer.md +14 -1
- package/assets/init/.qfai/assistant/agent/qa-gatekeeper.md +29 -0
- package/assets/init/.qfai/assistant/rule/constitution.md +5 -4
- package/assets/init/.qfai/assistant/rule/drift-protocol.md +14 -3
- package/assets/init/.qfai/assistant/rule/references/asset-authoring.md +70 -0
- package/assets/init/.qfai/assistant/rule/references/context-summary.md +30 -0
- package/assets/init/.qfai/assistant/rule/references/gate-failure-autorepair.md +42 -0
- package/assets/init/.qfai/assistant/rule/references/griller-recommendations.md +23 -0
- package/assets/init/.qfai/assistant/rule/references/host-backstops.md +60 -0
- package/assets/init/.qfai/assistant/rule/references/placeholder-scan.md +25 -0
- package/assets/init/.qfai/assistant/rule/references/reviewer-findings.md +41 -0
- package/assets/init/.qfai/assistant/rule/references/reviewer-remit.md +35 -0
- package/assets/init/.qfai/assistant/rule/shared-skill-delegation-baseline.md +26 -139
- package/assets/init/.qfai/assistant/rule/shared-skill-operating-baseline.md +6 -137
- package/assets/init/.qfai/assistant/rule/stage-cost.md +1 -1
- package/assets/init/.qfai/assistant/rule/test-layers.md +42 -0
- package/assets/init/.qfai/assistant/skill/qfai-configure/SKILL.md +8 -42
- package/assets/init/.qfai/assistant/skill/qfai-configure/references/principles.md +25 -0
- package/assets/init/.qfai/assistant/skill/qfai-discussion/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-discussion/references/discussion-completion-matrix.md +9 -2
- package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux/platform_baselines.md +11 -0
- package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux/review_audit_playbook.md +11 -0
- package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux_best_practices.md +12 -36
- package/assets/init/.qfai/assistant/skill/qfai-grill/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-grilling/SKILL.md +16 -88
- package/assets/init/.qfai/assistant/skill/qfai-grilling/references/delegated-session.md +58 -0
- package/assets/init/.qfai/assistant/skill/qfai-grilling/references/session-record.md +42 -0
- package/assets/init/.qfai/assistant/skill/qfai-implement/SKILL.md +8 -1
- package/assets/init/.qfai/assistant/skill/qfai-implement/references/credential-reuse.md +9 -0
- package/assets/init/.qfai/assistant/skill/qfai-implement/references/cross-spec-ownership.md +41 -0
- package/assets/init/.qfai/assistant/skill/qfai-implement/references/oracle-strength.md +6 -1
- package/assets/init/.qfai/assistant/skill/qfai-implement/references/red-admissibility.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-implement/references/ui-affecting.md +1 -3
- package/assets/init/.qfai/assistant/skill/qfai-maintain/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/SKILL.md +4 -4
- package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-guide.md +19 -5
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/design-md-spec.md +14 -5
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/generator-prompt.md +10 -0
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/handoff.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/iteration-loop.md +3 -4
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/review-payload-schema.md +10 -2
- package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/reviewer-prompt.md +16 -3
- package/assets/init/.qfai/assistant/skill/qfai-run/SKILL.md +6 -21
- package/assets/init/.qfai/assistant/skill/qfai-run/references/extraction.md +30 -10
- package/assets/init/.qfai/assistant/skill/qfai-run/references/operator-screens.md +34 -0
- package/assets/init/.qfai/assistant/skill/qfai-run/references/plan.md +28 -5
- package/assets/init/.qfai/assistant/skill/qfai-run/references/stage-points.md +29 -0
- package/assets/init/.qfai/assistant/skill/qfai-sdd/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/concrete-abstract-cycle.md +10 -1
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/contract-artifact-rules.md +21 -9
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-anti-patterns.md +19 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-authoring.md +11 -5
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-brand-catalog.md +17 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/requirements-decomposition.md +2 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-execution-playbook.md +5 -5
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-phase-checklists.md +4 -4
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-quality-gate.md +2 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-triage.md +47 -5
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/spec-traceability-rules.md +15 -3
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md +16 -1
- package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-design-contract-normalization.md +1 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/change-request.md +2 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/contracts/db-contract.sample.sql +2 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/03_contract/cli/cli-NNNN-title.md +2 -2
- package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/03_contract/tech.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/decisions.md +4 -0
- package/assets/init/.qfai/assistant/skill/qfai-triage/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-verify/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skill/qfai-verify/references/verify-output-contract.md +22 -8
- package/assets/init/.qfai/assistant/skill/web-research/SKILL.md +2 -10
- package/assets/init/.qfai/assistant/step/common-design-md/STEP.md +17 -3
- package/assets/init/.qfai/assistant/step/common-gate-run/STEP.md +30 -7
- package/assets/init/.qfai/assistant/step/discussion-uiux/STEP.md +11 -1
- package/assets/init/.qfai/assistant/step/implement-dep-bump/STEP.md +7 -0
- package/assets/init/.qfai/assistant/step/implement-scaffold/STEP.md +3 -3
- package/assets/init/.qfai/assistant/step/implement-seam/STEP.md +3 -1
- package/assets/init/.qfai/assistant/step/implement-tdd/STEP.md +3 -1
- package/assets/init/.qfai/assistant/step/implement-test-fix/STEP.md +2 -0
- package/assets/init/.qfai/assistant/step/sdd-flow/STEP.md +2 -0
- package/assets/init/.qfai/assistant/step/sdd-gate/STEP.md +4 -0
- package/assets/init/.qfai/assistant/step/sdd-triage/STEP.md +7 -3
- package/assets/init/.qfai/assistant/step/verify-change-note/STEP.md +8 -5
- package/assets/init/.qfai/assistant/step/verify-commit/STEP.md +6 -1
- package/assets/init/.qfai/assistant/step/verify-qfai-gate/STEP.md +20 -3
- package/assets/init/.qfai/assistant/step/verify-repeat-run/STEP.md +1 -1
- package/assets/init/.qfai/assistant/step/verify-repo-gate/STEP.md +23 -6
- package/assets/init/.qfai/waivers.yml +1 -1
- package/assets/init/root/.agents/rules/ai-readable-markdown.md +98 -0
- package/assets/init/root/.agents/rules/api-budget.md +4 -5
- package/assets/init/root/.agents/rules/documentation-clarity.md +30 -2
- package/assets/init/root/.agents/rules/grilling.md +4 -4
- package/assets/init/root/.agents/rules/interface-clarity.md +5 -5
- package/assets/init/root/.agents/rules/minimal-implementation.md +8 -0
- package/assets/init/root/.agents/rules/reminders.json +2 -2
- package/assets/init/root/.agents/rules/temporary-files.md +14 -5
- package/assets/init/root/.agents/rules/untrusted-content.md +3 -3
- package/assets/init/root/.agents/rules/user-questions.md +34 -7
- package/assets/init/root/.gitattributes +9 -0
- package/assets/init/root/.github/workflows/qfai-docs.yml +43 -11
- package/assets/init/root/.github/workflows/qfai-tests.yml +58 -15
- package/assets/init/root/.github/workflows/qfai-validate.yml +49 -42
- package/assets/init/root/AGENTS.md +1 -0
- package/assets/init/root/CLAUDE.md +1 -0
- package/assets/mdschema/story/03_contract/tech.mdschema.yml +7 -3
- package/assets/sandbox-templates/default-deny.yaml +0 -2
- package/assets/schemas/workflow/plan.schema.json +9 -2
- package/assets/scripts/check-mdschema.mjs +172 -5
- package/dist/cli/index.cjs +3979 -3055
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +3167 -2243
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +3024 -2500
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -9
- package/dist/index.d.ts +23 -9
- package/dist/index.mjs +2881 -2358
- package/dist/index.mjs.map +1 -1
- package/docs/MIGRATION-2.0.0.md +1 -1
- 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
|
|
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
|
-
- `
|
|
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
|
|
470
|
-
second run nothing there could fail on a downstream
|
|
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
|
|
481
|
-
|
|
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
|
|
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
|
|
@@ -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]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|