qfai 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/assets/defaults/agent-routing/qfai-implement.yml +9 -0
  2. package/assets/defaults/agent-routing/qfai-triage.yml +9 -0
  3. package/assets/defaults/workflows/answer-in-session.yml +8 -0
  4. package/assets/defaults/workflows/quarantine-flaky.yml +3 -0
  5. package/assets/defaults/workflows/repair-consistency.yml +1 -0
  6. package/assets/defaults/workflows/run-operation.yml +11 -0
  7. package/assets/defaults/workflows/write-procedure.yml +18 -0
  8. package/assets/graph/codegraph-native.mjs +174 -0
  9. package/assets/graph/graphify-native.py +72 -0
  10. package/assets/init/.claude/settings.json +2 -20
  11. package/assets/init/.codex/hooks.json +0 -12
  12. package/assets/init/.qfai/assistant/rule/constitution.md +31 -15
  13. package/assets/init/.qfai/assistant/rule/drift-protocol.md +62 -1
  14. package/assets/init/.qfai/assistant/rule/references/codex-delegation.md +27 -0
  15. package/assets/init/.qfai/assistant/rule/references/griller-recommendations.md +21 -9
  16. package/assets/init/.qfai/assistant/rule/review-convergence.md +25 -12
  17. package/assets/init/.qfai/assistant/rule/shared-skill-delegation-baseline.md +1 -0
  18. package/assets/init/.qfai/assistant/rule/shared-skill-operating-baseline.md +8 -3
  19. package/assets/init/.qfai/assistant/rule/workflow.md +2 -1
  20. package/assets/init/.qfai/assistant/skill/qfai-discussion/SKILL.md +6 -0
  21. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/discussion-completion-matrix.md +18 -9
  22. package/assets/init/.qfai/assistant/skill/qfai-grilling/SKILL.md +30 -20
  23. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/delegated-session.md +4 -3
  24. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/session-record.md +8 -2
  25. package/assets/init/.qfai/assistant/skill/qfai-implement/SKILL.md +7 -0
  26. package/assets/init/.qfai/assistant/skill/qfai-implement/references/ui-affecting.md +2 -2
  27. package/assets/init/.qfai/assistant/skill/qfai-run/SKILL.md +41 -39
  28. package/assets/init/.qfai/assistant/skill/qfai-run/references/codex-cloud-install.md +80 -0
  29. package/assets/init/.qfai/assistant/skill/qfai-run/references/extraction.md +36 -25
  30. package/assets/init/.qfai/assistant/skill/qfai-run/references/operator-screens.md +9 -1
  31. package/assets/init/.qfai/assistant/skill/qfai-run/references/plan.md +2 -0
  32. package/assets/init/.qfai/assistant/skill/qfai-run/references/reached-step-observations.md +132 -0
  33. package/assets/init/.qfai/assistant/skill/qfai-run/scripts/codex-cloud-install.mjs +117 -0
  34. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-authoring.md +20 -7
  35. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-phase-checklists.md +1 -1
  36. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-pre-draft-grilling.md +4 -1
  37. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-quality-gate.md +1 -1
  38. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-triage.md +29 -2
  39. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md +36 -4
  40. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/decisions.md +9 -0
  41. package/assets/init/.qfai/assistant/skill/qfai-triage/SKILL.md +10 -7
  42. package/assets/init/.qfai/assistant/skill/qfai-verify/references/verify-output-contract.md +7 -7
  43. package/assets/init/.qfai/assistant/step/common-design-md/STEP.md +33 -16
  44. package/assets/init/.qfai/assistant/step/common-gate-run/STEP.md +26 -0
  45. package/assets/init/.qfai/assistant/step/common-grilling-record/STEP.md +10 -2
  46. package/assets/init/.qfai/assistant/step/common-policy-check/STEP.md +1 -0
  47. package/assets/init/.qfai/assistant/step/common-review-cycle/STEP.md +23 -7
  48. package/assets/init/.qfai/assistant/step/discussion-interview/STEP.md +15 -8
  49. package/assets/init/.qfai/assistant/step/discussion-oq/STEP.md +5 -1
  50. package/assets/init/.qfai/assistant/step/discussion-pack/STEP.md +5 -1
  51. package/assets/init/.qfai/assistant/step/discussion-uiux/STEP.md +5 -1
  52. package/assets/init/.qfai/assistant/step/implement-dep-bump/STEP.md +8 -2
  53. package/assets/init/.qfai/assistant/step/implement-procedure/STEP.md +69 -0
  54. package/assets/init/.qfai/assistant/step/implement-quarantine/STEP.md +21 -4
  55. package/assets/init/.qfai/assistant/step/implement-regression-fix/STEP.md +3 -3
  56. package/assets/init/.qfai/assistant/step/implement-stress-harness/STEP.md +33 -2
  57. package/assets/init/.qfai/assistant/step/prototyping-preflight/STEP.md +8 -0
  58. package/assets/init/.qfai/assistant/step/sdd-triage/STEP.md +4 -2
  59. package/assets/init/.qfai/assistant/step/triage-answer/STEP.md +2 -2
  60. package/assets/init/.qfai/assistant/step/triage-operate/STEP.md +50 -0
  61. package/assets/init/.qfai/assistant/step/verify-qfai-gate/STEP.md +8 -1
  62. package/assets/init/.qfai/assistant/step/verify-repeat-run/STEP.md +40 -0
  63. package/assets/init/.qfai/assistant/step/verify-repo-gate/STEP.md +12 -0
  64. package/assets/init/root/.agents/rules/document-schema.md +17 -0
  65. package/assets/init/root/.agents/rules/documentation-clarity.md +1 -1
  66. package/assets/init/root/.agents/rules/graph-exploration.md +150 -0
  67. package/assets/init/root/.agents/rules/grilling.md +100 -43
  68. package/assets/init/root/.agents/rules/reminders.json +14 -18
  69. package/assets/init/root/.agents/rules/session-feedback.md +6 -11
  70. package/assets/init/root/.agents/rules/untrusted-content.md +2 -2
  71. package/assets/init/root/.agents/rules/user-questions.md +3 -3
  72. package/assets/init/root/.github/workflows/qfai-docs.yml +2 -2
  73. package/assets/init/root/.github/workflows/qfai-tests.yml +88 -35
  74. package/assets/init/root/.github/workflows/qfai-validate.yml +2 -2
  75. package/assets/init/root/AGENTS.md +1 -0
  76. package/assets/schemas/workflow/extraction.schema.json +6 -2
  77. package/assets/scripts/check-mdschema.mjs +77 -8
  78. package/dist/cli/index.cjs +2616 -1130
  79. package/dist/cli/index.cjs.map +1 -1
  80. package/dist/cli/index.mjs +2617 -1121
  81. package/dist/cli/index.mjs.map +1 -1
  82. package/dist/graph/cli.cjs +1568 -0
  83. package/dist/graph/cli.cjs.map +1 -0
  84. package/dist/graph/cli.d.cts +9 -0
  85. package/dist/graph/cli.d.ts +9 -0
  86. package/dist/graph/cli.mjs +1541 -0
  87. package/dist/graph/cli.mjs.map +1 -0
  88. package/dist/graph/hook.cjs +817 -0
  89. package/dist/graph/hook.cjs.map +1 -0
  90. package/dist/graph/hook.d.cts +9 -0
  91. package/dist/graph/hook.d.ts +9 -0
  92. package/dist/graph/hook.mjs +792 -0
  93. package/dist/graph/hook.mjs.map +1 -0
  94. package/dist/index.cjs +2734 -818
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.d.cts +129 -1
  97. package/dist/index.d.ts +129 -1
  98. package/dist/index.mjs +2707 -789
  99. package/dist/index.mjs.map +1 -1
  100. package/dist/process-C21nTvkT.d.cts +12 -0
  101. package/dist/process-C21nTvkT.d.ts +12 -0
  102. package/package.json +1 -1
@@ -216,6 +216,15 @@ routing:
216
216
  rerun_policy: changed-scope-dependents
217
217
  review_profile: default
218
218
 
219
+ - step: implement-procedure
220
+ phases:
221
+ - id: documentation
222
+ mandatory_agents: [doc-steward]
223
+ conditional_agents: []
224
+ parallel_groups: []
225
+ rerun_policy: changed-scope-dependents
226
+ review_profile: default
227
+
219
228
  # Writes the bodies of empty acceptance tests, keeping each annotation.
220
229
  - step: implement-acceptance
221
230
  phases:
@@ -4,6 +4,15 @@
4
4
  routing:
5
5
  # The triage steps change no tracked file, so each has one author and no
6
6
  # build or test phase.
7
+ - step: triage-operate
8
+ phases:
9
+ - id: operate
10
+ mandatory_agents: [devops-ci-engineer]
11
+ conditional_agents: []
12
+ parallel_groups: []
13
+ rerun_policy: changed-scope-dependents
14
+ review_profile: default
15
+
7
16
  - step: triage-owned-cleanup
8
17
  phases:
9
18
  - id: cleanup
@@ -0,0 +1,8 @@
1
+ route: answer-in-session
2
+ family: close
3
+ stages:
4
+ - id: answer
5
+ kind: triage
6
+ steps: [triage-answer]
7
+ decisionPoints: []
8
+ branchPoints: []
@@ -29,6 +29,9 @@ stages:
29
29
  after: [note]
30
30
  decisionPoints: []
31
31
  branchPoints:
32
+ - step: implement-quarantine
33
+ outcomes:
34
+ - { outcome: failing-consistently, routes: decision-table }
32
35
  - step: implement-diagnose
33
36
  outcomes:
34
37
  - { outcome: product-race, routes: [fix-intermittent] }
@@ -11,6 +11,7 @@ stages:
11
11
  - { step: sdd-flow, passThrough: true }
12
12
  - { step: sdd-story, passThrough: true }
13
13
  - { step: sdd-contract, passThrough: true }
14
+ - { step: common-design-md, passThrough: true }
14
15
  - { step: sdd-cycle, passThrough: true }
15
16
  after: [diagnose]
16
17
  review: spec
@@ -0,0 +1,11 @@
1
+ route: run-operation
2
+ family: upkeep
3
+ stages:
4
+ - id: operate
5
+ kind: triage
6
+ steps: [triage-operate]
7
+ decisionPoints: []
8
+ branchPoints:
9
+ - step: triage-operate
10
+ outcomes:
11
+ - { outcome: needs-change, routes: decision-table }
@@ -0,0 +1,18 @@
1
+ route: write-procedure
2
+ family: change
3
+ stages:
4
+ - id: documentation
5
+ kind: documentation
6
+ steps: [implement-procedure]
7
+ - id: note
8
+ kind: verify
9
+ steps:
10
+ - { step: verify-change-note, passThrough: true }
11
+ after: [documentation]
12
+ review: code
13
+ - id: verify
14
+ kind: verify
15
+ steps: [verify-qfai-gate, verify-repo-gate, verify-commit]
16
+ after: [note]
17
+ decisionPoints: []
18
+ branchPoints: []
@@ -0,0 +1,174 @@
1
+ import process from "node:process";
2
+ import path from "node:path";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ async function main() {
7
+ const sdk = await import(pathToFileURL(process.argv[2]).href);
8
+ const { default: CodeGraph, setLogger, silentLogger } = sdk.default;
9
+ setLogger(silentLogger);
10
+ const mode = process.argv[4];
11
+ const corpus = process.argv[3];
12
+ const graph = mode
13
+ ? await CodeGraph.open(process.argv[3], { readOnly: true })
14
+ : await CodeGraph.init(process.argv[3], { index: false });
15
+ try {
16
+ if (mode === "inventory") {
17
+ process.stdout.write(
18
+ JSON.stringify({ files: graph.getFiles().map((file) => file.path) }) + "\n",
19
+ );
20
+ } else if (mode === "coverage") {
21
+ process.stdout.write(JSON.stringify(await coverage(graph, corpus)) + "\n");
22
+ } else if (mode === undefined) {
23
+ const result = await graph.indexAll();
24
+ const build = graph.getIndexBuildInfo();
25
+ await writeFile(
26
+ path.join(corpus, ".codegraph", "qfai-native-evidence.json"),
27
+ JSON.stringify({
28
+ engineVersion: build.version,
29
+ extractionVersion: build.extractionVersion,
30
+ lastIndexedAt: graph.getLastIndexedAt(),
31
+ result: {
32
+ success: result.success,
33
+ filesDiscovered: result.filesDiscovered ?? null,
34
+ filesIndexed: result.filesIndexed,
35
+ filesSkipped: result.filesSkipped,
36
+ filesErrored: result.filesErrored,
37
+ filesSkippedUnsupported: result.filesSkippedUnsupported ?? 0,
38
+ topUnsupportedExtensions: result.topUnsupportedExtensions ?? [],
39
+ errors: result.errors.map((error) => ({
40
+ code: error.code ?? "unclassified",
41
+ severity: error.severity,
42
+ filePath: safeRelativeInput(corpus, error.filePath),
43
+ })),
44
+ },
45
+ }) + "\n",
46
+ );
47
+ process.stdout.write("CodeGraph native indexing completed.\n");
48
+ } else {
49
+ throw new Error("Unsupported native operation");
50
+ }
51
+ } finally {
52
+ graph.close();
53
+ }
54
+ }
55
+
56
+ function safeRelativeInput(corpus, file) {
57
+ if (typeof file !== "string" || !file) return "Unidentified native input";
58
+ const relative = (path.isAbsolute(file) ? path.relative(corpus, file) : file).replaceAll(
59
+ "\\",
60
+ "/",
61
+ );
62
+ if (
63
+ path.posix.isAbsolute(relative) ||
64
+ path.win32.isAbsolute(relative) ||
65
+ relative.split("/").some((part) => !part || part === "." || part === "..") ||
66
+ /(?:^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.pypirc$|\.netrc$|credentials?(?:\.|$)|secrets?(?:\.|$))|\.(?:pem|key|p12|pfx)$/i.test(
67
+ relative,
68
+ )
69
+ )
70
+ return "Unidentified native input";
71
+ return relative;
72
+ }
73
+
74
+ async function coverage(graph, corpus) {
75
+ const unknown = { complete: false, unsupported: [], unreadable: [], unparsed: [] };
76
+ let evidence;
77
+ try {
78
+ evidence = JSON.parse(
79
+ await readFile(path.join(corpus, ".codegraph", "qfai-native-evidence.json"), "utf8"),
80
+ );
81
+ } catch {
82
+ return unknown;
83
+ }
84
+ const build = graph.getIndexBuildInfo();
85
+ const result = evidence?.result;
86
+ const counts = [
87
+ "filesDiscovered",
88
+ "filesIndexed",
89
+ "filesSkipped",
90
+ "filesErrored",
91
+ "filesSkippedUnsupported",
92
+ ];
93
+ if (
94
+ !record(evidence) ||
95
+ Object.keys(evidence).length !== 4 ||
96
+ evidence.engineVersion !== build.version ||
97
+ evidence.extractionVersion !== build.extractionVersion ||
98
+ !Number.isSafeInteger(evidence.lastIndexedAt) ||
99
+ evidence.lastIndexedAt !== graph.getLastIndexedAt() ||
100
+ !record(result) ||
101
+ Object.keys(result).length !== 8 ||
102
+ typeof result.success !== "boolean" ||
103
+ counts.some((key) => !Number.isSafeInteger(result[key]) || result[key] < 0) ||
104
+ !Array.isArray(result.errors) ||
105
+ !Array.isArray(result.topUnsupportedExtensions)
106
+ )
107
+ return unknown;
108
+ const unsupported = [];
109
+ const unreadable = [];
110
+ const unparsed = [];
111
+ let documentedInputs = 0;
112
+ for (const group of result.topUnsupportedExtensions) {
113
+ if (
114
+ !record(group) ||
115
+ typeof group.ext !== "string" ||
116
+ !Number.isSafeInteger(group.count) ||
117
+ group.count < 0
118
+ )
119
+ return unknown;
120
+ if (
121
+ /^\.(?:md|mdx|txt|rst|adoc|pdf|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|mp[34]|wav)$/i.test(
122
+ group.ext,
123
+ )
124
+ ) {
125
+ documentedInputs += group.count;
126
+ } else {
127
+ unsupported.push(`${group.ext} (${group.count} inputs)`);
128
+ }
129
+ }
130
+ const namedUnsupported = result.topUnsupportedExtensions.reduce(
131
+ (count, group) => count + group.count,
132
+ 0,
133
+ );
134
+ if (namedUnsupported < result.filesSkippedUnsupported)
135
+ unsupported.push("Other unsupported native inputs");
136
+ if (
137
+ namedUnsupported > result.filesSkippedUnsupported ||
138
+ documentedInputs > result.filesSkippedUnsupported
139
+ )
140
+ return unknown;
141
+ for (const error of [
142
+ ...result.errors,
143
+ ...graph.getFiles().flatMap((file) => file.errors ?? []),
144
+ ]) {
145
+ if (!record(error) || !["error", "warning"].includes(error.severity)) return unknown;
146
+ const file = safeRelativeInput(corpus, error.filePath);
147
+ if (error.code === "read_error") unreadable.push(file);
148
+ else unparsed.push(file);
149
+ }
150
+ if (result.filesErrored > 0 && unreadable.length === 0 && unparsed.length === 0)
151
+ unparsed.push("Unidentified native extraction errors");
152
+ const complete =
153
+ result.success &&
154
+ result.filesIndexed + result.filesSkipped + result.filesErrored >= result.filesDiscovered &&
155
+ result.filesErrored === 0 &&
156
+ unsupported.length === 0 &&
157
+ unreadable.length === 0 &&
158
+ unparsed.length === 0;
159
+ return {
160
+ complete,
161
+ unsupported,
162
+ unreadable: [...new Set(unreadable)],
163
+ unparsed: [...new Set(unparsed)],
164
+ };
165
+ }
166
+
167
+ function record(value) {
168
+ return typeof value === "object" && value !== null && !Array.isArray(value);
169
+ }
170
+
171
+ main().catch(() => {
172
+ process.stderr.write("CodeGraph native indexing failed.\n");
173
+ process.exitCode = 1;
174
+ });
@@ -0,0 +1,72 @@
1
+ """Inspect the managed corpus with Graphify's native freshness operation."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from graphify.detect import detect_incremental
8
+
9
+
10
+ def input_inventory(output, graph):
11
+ manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8"))
12
+ if not isinstance(manifest, dict):
13
+ raise ValueError("Invalid native manifest")
14
+ files = set(manifest.keys())
15
+ groups = [graph["nodes"]]
16
+ for name in ("edges", "links", "hyperedges"):
17
+ if name in graph:
18
+ groups.append(graph[name])
19
+ nested = graph.get("graph", {})
20
+ if not isinstance(nested, dict):
21
+ raise ValueError("Invalid native graph attributes")
22
+ if "hyperedges" in nested:
23
+ groups.append(nested["hyperedges"])
24
+ for group in groups:
25
+ if not isinstance(group, list):
26
+ raise ValueError("Invalid native graph references")
27
+ for item in group:
28
+ if not isinstance(item, dict):
29
+ raise ValueError("Invalid native graph reference")
30
+ for field in ("source_file", "definition_file", "path"):
31
+ value = item.get(field)
32
+ if value is None or value == "":
33
+ continue
34
+ if not isinstance(value, str):
35
+ raise ValueError("Invalid native input path")
36
+ files.add(value)
37
+ return sorted(files)
38
+
39
+
40
+ def main():
41
+ root = Path(sys.argv[1])
42
+ kind = sys.argv[2]
43
+ output = root / "graphify-out"
44
+ graph = json.loads((output / "graph.json").read_text(encoding="utf-8"))
45
+ if not isinstance(graph, dict) or not isinstance(graph.get("nodes"), list):
46
+ raise ValueError("Invalid native graph")
47
+ if kind == "inventory":
48
+ print(json.dumps({"files": input_inventory(output, graph)}))
49
+ return
50
+ if kind not in ("semantic", "ast"):
51
+ raise ValueError("Unsupported native inspection")
52
+ state = detect_incremental(
53
+ root,
54
+ manifest_path=str(output / "manifest.json"),
55
+ kind=kind,
56
+ cache_root=output,
57
+ )
58
+ print(json.dumps({
59
+ "current": state["new_total"] == 0
60
+ and not state["deleted_files"]
61
+ and not state["excluded_files"],
62
+ "unsupported": state.get("unclassified", []),
63
+ "unreadable": state.get("unreadable", []),
64
+ }))
65
+
66
+
67
+ if __name__ == "__main__":
68
+ try:
69
+ main()
70
+ except Exception:
71
+ sys.stderr.write("Graphify freshness inspection failed.\n")
72
+ sys.exit(1)
@@ -30,7 +30,7 @@
30
30
  "command": "node",
31
31
  "args": [
32
32
  "-e",
33
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
33
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake|ci-monitor-event)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
34
34
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
35
35
  "structured-question"
36
36
  ]
@@ -46,7 +46,7 @@
46
46
  "command": "node",
47
47
  "args": [
48
48
  "-e",
49
- "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
49
+ "try{const f=require('fs');let i={};try{i=JSON.parse(f.readFileSync(0,'utf8'))||{}}catch{}const p=i.prompt;if(!(typeof p==='string'&&/^[ \\t]*(<(task-notification|wake|ci-monitor-event)[\\s>]|\\[SYSTEM NOTIFICATION)/m.test(p))){let n=1;if(typeof i.session_id==='string'){try{const t=require('path').join(require('os').tmpdir(),['qfai-reminder',i.session_id,i.agent_id||'main',process.argv[2]].join('-').replace(/[^\\w.-]/g,'_'));f.appendFileSync(t,'.');n=f.statSync(t).size}catch{}}const e=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(e!==undefined){const {briefContext,...m}=e;if(n>1&&typeof briefContext==='string')m.hookSpecificOutput.additionalContext=briefContext;console.log(JSON.stringify(m))}}}catch{}",
50
50
  "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
51
51
  "free-text-entry"
52
52
  ]
@@ -198,24 +198,6 @@
198
198
  }
199
199
  ]
200
200
  }
201
- ],
202
- "Stop": [
203
- {
204
- "matcher": "*",
205
- "hooks": [
206
- {
207
- "type": "command",
208
- "statusMessage": "QFAI session feedback reminder",
209
- "command": "node",
210
- "args": [
211
- "-e",
212
- "try{const f=require('fs');let i;try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{process.exit(0)}const l=typeof i.last_assistant_message==='string'?i.last_assistant_message.trim():'';if(i.stop_hook_active===true||(l.length>0&&('?'+String.fromCharCode(65311)).includes(l.slice(-1))))process.exit(0);const m=JSON.parse(f.readFileSync(process.argv[1],'utf8'))[process.argv[2]];if(m!==undefined)console.log(JSON.stringify(m))}catch{}",
213
- "${CLAUDE_PROJECT_DIR}/.agents/rules/reminders.json",
214
- "session-feedback"
215
- ]
216
- }
217
- ]
218
- }
219
201
  ]
220
202
  }
221
203
  }
@@ -107,18 +107,6 @@
107
107
  }
108
108
  ]
109
109
  }
110
- ],
111
- "Stop": [
112
- {
113
- "hooks": [
114
- {
115
- "type": "command",
116
- "statusMessage": "QFAI session feedback reminder",
117
- "command": "node -e \"try{const f=require('fs'),p=require('path');let d=process.cwd(),r;for(;;){const c=p.join(d,'.agents','rules','reminders.json');if(r===undefined&&f.existsSync(c))r=c;if(f.existsSync(p.join(d,'.git')))break;const u=p.dirname(d);if(u===d){r=undefined;break}d=u}if(r){let i;try{i=JSON.parse(f.readFileSync(0,'utf8'))}catch{process.exit(0)}const l=typeof i.last_assistant_message==='string'?i.last_assistant_message.trim():'';if(i.stop_hook_active===true||(l.length>0&&('?'+String.fromCharCode(65311)).includes(l.slice(-1))))process.exit(0);const m=JSON.parse(f.readFileSync(r,'utf8'))[process.argv[1]];m===undefined||console.log(JSON.stringify(m))}}catch{}\" session-feedback",
118
- "timeout": 10
119
- }
120
- ]
121
- }
122
110
  ]
123
111
  }
124
112
  }
@@ -3,6 +3,13 @@
3
3
  This document defines **non‑negotiable operating rules** for QFAI agents and subagents.
4
4
  It is inspired by proven “constitution / articles / guardrails” patterns in existing SDD toolchains, but adapted to QFAI’s minimal workflow.
5
5
 
6
+ Actual user instructions take priority over repository guidelines. Recorded
7
+ applicable user authority is defined in
8
+ `.agents/rules/grilling.md#explicit-delegation-for-a-discussion`: an explicit
9
+ in-scope delegation may authorize reversible requested judgment without a
10
+ fictional individual approval answer. It supplies no unknown fact or authority
11
+ for an operation outside its scope.
12
+
6
13
  Inside an `npx qfai workflow` run, what authorizes the run's work and the one target a work order binds its stage to are stated in `.qfai/assistant/rule/shared-skill-operating-baseline.md#what-authorizes-a-runs-work`. They add to these articles and except none.
7
14
 
8
15
  ---
@@ -260,7 +267,7 @@ sanctioned move, and neither is asking a sixth clarification anyway.
260
267
  **A grilling session survives exhaustion too**, and for the same reason: its
261
268
  questions never spent budget, so there is none left to run out of. A session
262
269
  already under way continues to its own end condition, a delegated one still
263
- putting its critical decisions to the user, and a stage reached after
270
+ requiring actual user authority for critical decisions, and a stage reached after
264
271
  exhaustion still opens one where its work calls for it. Treating a spent budget
265
272
  as the end of a session would put the design decisions back where this article
266
273
  found them — settled quietly, on an assumption nobody was asked about.
@@ -322,16 +329,18 @@ If confidence is low, inspect the repository further.
322
329
 
323
330
  **Stop on detection.** Where a contradiction in the spec, an unconsidered case,
324
331
  or a technical obstacle surfaces mid-run, stop rather than deciding alone.
325
- Settle it between the agents, and put a critical decision to the user
326
- (`.agents/rules/grilling.md#critical-decisions`). A contradiction with the spec
327
- is critical. Nothing is recorded beyond the stage's final report.
332
+ Settle it between the agents. A critical decision needs an actual user answer
333
+ or recorded applicable authorization under
334
+ `.agents/rules/grilling.md#explicit-delegation-for-a-discussion`; without it,
335
+ ask when permitted or register it open. A contradiction with the spec is
336
+ critical. Nothing is recorded beyond the stage's final report.
328
337
 
329
338
  **Only one outcome is the Drift Protocol's.**
330
339
 
331
- | The stop concludes | What follows |
332
- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
333
- | Settled input must change | `.qfai/assistant/rule/drift-protocol.md`: stop the dependent work, raise the Change Request, wait for approval |
334
- | The obstacle is this run's to solve — an unavailable dependency, an approach that failed | The run solves it. Nothing upstream changes, so there is nothing to approve |
340
+ | The stop concludes | What follows |
341
+ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
342
+ | Settled input must change | `.qfai/assistant/rule/drift-protocol.md`: stop dependent work, raise the Change Request, record actual applicable user authority before changing it |
343
+ | The obstacle is this run's to solve — an unavailable dependency, an approach that failed | The run solves it. Nothing upstream changes, so there is nothing to approve |
335
344
 
336
345
  Stopping does not change settled input and is not a second way to. What it
337
346
  contributes to a Change Request is what that protocol asks of the class: for
@@ -366,7 +375,9 @@ rule master names.
366
375
  Rules:
367
376
 
368
377
  0. **A no-question mode is read first.** Where rule 4 applies, no question is put
369
- at all, so there is nothing whose availability rules 1 to 3 could judge. Read
378
+ to the user, so there is nothing whose availability rules 1 to 3 could judge.
379
+ Actual qualifying explicit delegation may still permit agent-to-agent rounds
380
+ under the master; no-question mode alone does not. Read
370
381
  the other way round, a mode that withholds the tool would make it unavailable,
371
382
  route the question to rule 3's plain-text fallback, and hand the agent a rule
372
383
  that says to ask beside one that says not to.
@@ -406,12 +417,14 @@ Rules:
406
417
  this rule is activated by the `--auto` flag alone.
407
418
  6. **A grilling session does not reach the user under `--auto`.** Its questions
408
419
  are exempt from the Article VI budget, not from rule 4: a no-question mode
409
- asks nothing, whatever the question is for. The session still runs — it
420
+ sends no question to the user, whatever it is for. The session still runs — it
410
421
  settles what the repository settles, dispatches sub-agents for the facts, and
411
- in a delegated session adopts the griller's recommendation for every decision
412
- that is not critical — and every **node** left over (in a delegated session,
413
- each critical decision and each fact only the user holds) is **opened as a
414
- question in the register the stage reads**, so the stage cannot complete over it. Every node, not every
422
+ in a delegated session adopts the griller's recommendation for non-critical
423
+ decisions. A critical decision needs an actual user answer or recorded
424
+ applicable authorization; without it the node stays open. It opens every **node**
425
+ left over, including uncovered critical decisions and unavailable user-held
426
+ facts, **as a question in the register the stage reads**, so the
427
+ stage cannot complete over it. Every node, not every
415
428
  decision: a fact only the user holds cannot be settled from evidence either,
416
429
  and opening the decisions while dropping the facts loses exactly the nodes no
417
430
  lookup could have reached. Where a document requires the field to hold
@@ -419,7 +432,10 @@ Rules:
419
432
  open question; a labelled value under an open question is not a settled
420
433
  decision. What rule 4 does not license is the assumption **alone**, which
421
434
  reads as settled to whoever finds it next. A fact declared undefaultable has
422
- no value to write down at all: the run stops and names it.
435
+ no value to write down at all: the run stops and names it. A qualifying
436
+ explicitly delegated session ends `adopted` only with the master's empty-tree,
437
+ actual-input and authority conditions met. No-question mode alone cannot
438
+ close a node or authorize a reserved decision.
423
439
 
424
440
  This article survives context compaction because `.qfai/assistant/rule/constitution.md` is a P1 reload target.
425
441
 
@@ -24,6 +24,10 @@ The sole Approach-change exception is the published decision prefix recovery
24
24
  below. It preserves the target's ID, Content, Status and complete original
25
25
  Approach suffix. It excludes inherited rows and open-question rows.
26
26
 
27
+ The sole row-removal exception is the merge-added identical decision copy
28
+ recovery below. It removes only proven raw-identical surplus copies of a
29
+ decision and keeps one original. It excludes open-question rows.
30
+
27
31
  ## Allowed exceptions (minimal whitelist)
28
32
 
29
33
  - A project may add a local overlay beside a shipped rule. The overlay adds project guidance; it does not repeal a shipped rule.
@@ -81,6 +85,60 @@ recovery authority and clear no form error. Inherited malformed records need a
81
85
  separate owning change request; this operation cannot repair them. These are
82
86
  owner instructions, not an automatic recovery command or a concurrency guarantee.
83
87
 
88
+ ## Merge-added identical decision copy recovery
89
+
90
+ The SDD owner may manually remove only raw-identical surplus copies of a
91
+ decision row that an exact two-parent bad merge introduced. Approval of this
92
+ protocol authorizes no target. First append a separate new in-force change
93
+ request that names the exact `decisions.md` path and the decision ID.
94
+
95
+ 1. Name the bad merge. It must be an available ancestor of current HEAD with
96
+ exactly two parents, and it must expose the introduced surplus. Name both
97
+ parents and their unique common ancestor. Each of the three must hold exactly
98
+ one byte-identical original of the row. Fix the explicit base and its unique
99
+ comparison ancestor; both must hold one original and no surplus selected for
100
+ removal. A missing or non-ancestor merge, a third parent, a non-unique
101
+ ancestor or a source that does not hold exactly one original stops recovery.
102
+ 2. Read the complete stored UTF-8 raw row of every selected current occurrence:
103
+ all four cells, delimiters, padding and line ending. Hash each with SHA-256,
104
+ including the line ending. Do not trim, decode escapes or normalize endings.
105
+ Nonidentical cells, padding or endings are a different collision and are
106
+ outside this recovery.
107
+ 3. Record in the request the path, the ID, the bad merge, both parents, the
108
+ ancestor, the base and its comparison ancestor, the location of each
109
+ occurrence, the raw hash, the retain/remove mapping, the actual authority,
110
+ the residual risk and the rollback. Record actual authority; invent no
111
+ answer. The request fixes input lineage only.
112
+ 4. After the request is appended, have the evidence for the write reviewed
113
+ independently and adopted. It fixes the actual write HEAD, base and
114
+ comparison ancestor, the complete preimage and replacement bytes and hashes,
115
+ the mapping and the current authority. The request does not name its own
116
+ commit, so the actual write revision stays distinct from the lineage above.
117
+ This adds no receipt grammar and no validator input.
118
+ 5. Immediately before writing, recheck the refs, their availability, the path,
119
+ the authority, the occurrence count, the mapping and the bytes. Stale,
120
+ missing, ambiguous, conflicting or changed proof stops before writing.
121
+ Changed observations need refreshed reviewed evidence. A changed target or
122
+ operation needs a new request; do not rewrite the cells of an earlier one.
123
+ 6. Remove only the mapped surplus occurrences. Keep one complete original, every
124
+ distinct decision and reference, every unrelated current byte and the Git
125
+ history. Do not change a Status, rewrite a reference or reorder other
126
+ history. Open-question rows, nonidentical collisions, ordinary duplicate
127
+ cleanup and surplus that the base or its comparison ancestor already holds
128
+ are excluded.
129
+ 7. Rollback restores the recorded complete preimage only while the current file
130
+ is byte-identical to its owned replacement. Otherwise keep the current bytes,
131
+ stop and name the unrestored path. Restoring brings back the duplicate
132
+ failure. Do not reset, amend or force push.
133
+
134
+ The existing duplicate check, positional drift, consumed-DONE authorization and
135
+ valid Status-only rules keep applying without an exemption. Other drift that
136
+ the file already has stays diagnosed; it is not a prerequisite for this
137
+ correction. An actual required stage gate still stops dependent work. Correcting
138
+ the copies is not a whole-target PASS, and the request stays at WIP while a named
139
+ action or required check is unfinished. These are owner instructions, not a
140
+ command, an executor or a concurrency, atomicity or power-loss guarantee.
141
+
84
142
  ## Drift classes
85
143
 
86
144
  Intent drift means an approved obligation should change. Record the current obligation, the proposed behavior, realistic
@@ -168,6 +226,9 @@ copy of a locked file before deciding its lock is wrong.
168
226
  ## Non-negotiable constraints
169
227
 
170
228
  - Downstream stages do not patch protected upstream artifacts before an in-force Change request: decision row authorizes the path. The drift profile compares the branch against baseBranch and reports QFAI-DRIFT-001 for an unapproved protected change. The SDD owner may create the request row itself without a prior row.
171
- - Existing decisions.md and open-questions.md rows retain their ID, Content, and Approach, except for the explicit branch-added decision renumbering and published decision prefix recovery above. Inherited rows retain those cells without exception. A former row may change Status; new content is appended as a new row.
229
+ QFAI-DRIFT-003 is a warning that nothing was compared, because baseBranch does not resolve.
230
+ QFAI-DRIFT-004 is a warning that an uncommitted edit to a protected file is not compared:
231
+ record the Change request row before you commit the change.
232
+ - Existing decisions.md and open-questions.md rows retain their ID, Content, and Approach, except for the explicit branch-added decision renumbering, published decision prefix recovery and merge-added identical decision copy recovery above. Inherited rows retain those cells without exception. A former row may change Status; new content is appended as a new row.
172
233
  - Vendored assistant rules are changed in the package and synchronized into projects by `npx qfai init --force`, which overwrites a local edit. A project rule lives in a `*.local.md` overlay beside the vendored one.
173
234
  - When approval is unavailable, keep the affected items stopped and report the decision needed. Continue unrelated work.
@@ -0,0 +1,27 @@
1
+ # Codex Delegation
2
+
3
+ Read before the first `spawn_agent` call when the host is Codex.
4
+
5
+ ## Codex delegation notes
6
+
7
+ A sub-agent should cost the orchestrator one brief and one final answer. A call
8
+ that only checks progress, such as a short wait, a sleep or an agent listing,
9
+ is one more model call, so the orchestrator makes none.
10
+
11
+ - **Self-contained brief.** Call `spawn_agent` with `fork_turns: "none"` and a
12
+ `message` that holds everything the sub-agent needs: the task, the files to
13
+ read, the paths it may write, the checks to run and the shape of its answer.
14
+ `fork_turns` defaults to `"all"`, which copies the whole parent thread into
15
+ the sub-agent. Where the tool takes `fork_context` instead, leave it out or
16
+ pass `false`.
17
+ - **One final answer, no interim messages.** The brief tells the sub-agent to
18
+ send no `send_message` updates and to return its result once, as its final
19
+ answer. Where `wait_agent` takes no `targets`, every queued message ends the
20
+ orchestrator's wait early.
21
+ - **Long wait.** With no work of its own left, the orchestrator calls
22
+ `wait_agent` with a `timeout_ms` of minutes within the maximum the tool states,
23
+ such as `600000`, not 10 to 60 seconds. A wait that times out is
24
+ followed by another long wait.
25
+ - **No polling.** Never wait with `sleep`, a loop of short `wait_agent` calls or
26
+ repeated `list_agents` calls. `followup_task` gives a sub-agent a new task; it
27
+ never asks for progress.
@@ -6,15 +6,27 @@ Read when a griller's recommendation is used.
6
6
 
7
7
  A grilling session puts a recommended answer beside each question (`.agents/rules/grilling.md`). Who settled the decision decides what happens next.
8
8
 
9
- | The decision was settled | What follows |
10
- | ----------------------------------------------------- | --------------------------------------------------------------------------------- |
11
- | By the user, from the recommendation among the inputs | The decision is theirs. The griller may review the artifact |
12
- | Agent to agent, not critical | The decision stands. The griller that recommended it does not review the artifact |
13
- | Agent to agent, critical, with no user adjudication | The artifact is wrong, and no reviewer can clear it |
14
-
15
- **The second row is how a delegated session is meant to end** (`.agents/rules/grilling.md`), not a finding. The reviewer checks that the decision is in the final report and is not critical. A reviewer that doubts its merit raises that as an ordinary finding against the artifact, under its own remit, as it would for any other content.
16
-
17
- **The third row is not a routing problem.** A critical decision is the user's in every session, and a run that could not ask records it as an open question rather than adopting it. An agent-adopted critical decision is therefore an artifact carrying something nobody with the standing decided, and handing it to a different reviewer would launder it.
9
+ | The decision was settled | What follows |
10
+ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
11
+ | By the user, from the recommendation among the inputs | The decision is theirs. The griller may review the artifact |
12
+ | Agent to agent, non-critical or with recorded applicable human authority | The decision stands. The griller that recommended it does not review the artifact |
13
+ | Agent to agent, critical, without an actual user answer or recorded applicable authority | The artifact is wrong, and no reviewer can clear it |
14
+
15
+ **The second row is how a delegated session is meant to end**
16
+ (`.agents/rules/grilling.md`), not a finding. The reviewer checks the final
17
+ report and, for a critical decision, the actual
18
+ applicable user authority defined in
19
+ `.agents/rules/grilling.md#explicit-delegation-for-a-discussion`.
20
+ An explicit delegation covering reversible requested judgment is authority,
21
+ not an individual human option answer. The recommending griller remains
22
+ disqualified from reviewing an agent adoption. A reviewer that doubts its merit
23
+ raises an ordinary finding against the artifact under its own remit.
24
+
25
+ **The third row is not a routing problem.** Without an actual answer or
26
+ recorded applicable user authority, a critical decision stays open. A run that
27
+ cannot ask records it as an open question rather than adopting it. Agent
28
+ agreement cannot supply authority, and a different reviewer cannot repair its
29
+ absence. No-question mode alone authorizes no decision.
18
30
  The reviewer must return `REVISE` and name the decision: it is reopened and put to the user, or recorded open where no question can be asked.
19
31
 
20
32
  The first row needs a reason, because the intuitive one is wrong. A sub-agent starting with a reset context cannot defer to something it does not remember, so deference is not the risk. **Correlation** is: a fresh instance of the same agent, on the same model, over the same evidence, re-derives the preference that produced the recommendation and finds it good on the merits. Resetting the