autonomous-sdlc-harness 0.1.0 → 0.4.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 (121) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/docs.js +220 -0
  4. package/dist/commands/docs.js.map +1 -0
  5. package/dist/commands/doctor.js +32 -11
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.js +240 -64
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/registry.js +2 -0
  10. package/dist/commands/registry.js.map +1 -1
  11. package/dist/config/check.js +36 -9
  12. package/dist/config/check.js.map +1 -1
  13. package/dist/config/model.js +43 -1
  14. package/dist/config/model.js.map +1 -1
  15. package/dist/core/git.js +29 -0
  16. package/dist/core/git.js.map +1 -1
  17. package/dist/core/layerGapRemedy.js +3 -2
  18. package/dist/core/layerGapRemedy.js.map +1 -1
  19. package/dist/core/paths.js +22 -2
  20. package/dist/core/paths.js.map +1 -1
  21. package/dist/core/pluginIdentity.js +33 -0
  22. package/dist/core/pluginIdentity.js.map +1 -0
  23. package/dist/core/prompt.js +6 -2
  24. package/dist/core/prompt.js.map +1 -1
  25. package/dist/core/report.js +9 -0
  26. package/dist/core/report.js.map +1 -1
  27. package/dist/core/writer.js +24 -5
  28. package/dist/core/writer.js.map +1 -1
  29. package/dist/detect/presets.js +35 -26
  30. package/dist/detect/presets.js.map +1 -1
  31. package/dist/detect/signals.js +13 -9
  32. package/dist/detect/signals.js.map +1 -1
  33. package/dist/doctor/checks.js +483 -38
  34. package/dist/doctor/checks.js.map +1 -1
  35. package/dist/generators/claudeContext.js +4 -5
  36. package/dist/generators/claudeContext.js.map +1 -1
  37. package/dist/generators/githubWorkflows.js +66 -0
  38. package/dist/generators/githubWorkflows.js.map +1 -0
  39. package/dist/generators/harnessConfig.js +13 -5
  40. package/dist/generators/harnessConfig.js.map +1 -1
  41. package/dist/generators/notifications.js +105 -19
  42. package/dist/generators/notifications.js.map +1 -1
  43. package/dist/generators/outerLoopScripts.js +30 -3
  44. package/dist/generators/outerLoopScripts.js.map +1 -1
  45. package/dist/generators/permissionProfile.js +152 -21
  46. package/dist/generators/permissionProfile.js.map +1 -1
  47. package/dist/generators/projectSettings.js +5 -15
  48. package/dist/generators/projectSettings.js.map +1 -1
  49. package/dist/generators/repoRoot.js +148 -25
  50. package/dist/generators/repoRoot.js.map +1 -1
  51. package/dist/generators/scripts.js +4 -1
  52. package/dist/generators/scripts.js.map +1 -1
  53. package/dist/generators/stateDir.js +9 -3
  54. package/dist/generators/stateDir.js.map +1 -1
  55. package/dist/machine/paths.js +19 -4
  56. package/dist/machine/paths.js.map +1 -1
  57. package/dist/machine/plugins.js +2 -1
  58. package/dist/machine/plugins.js.map +1 -1
  59. package/dist/remote/githubActions.js +86 -0
  60. package/dist/remote/githubActions.js.map +1 -0
  61. package/dist/retrieval/chunk.js +158 -0
  62. package/dist/retrieval/chunk.js.map +1 -0
  63. package/dist/retrieval/corpus.js +75 -0
  64. package/dist/retrieval/corpus.js.map +1 -0
  65. package/dist/retrieval/models.js +175 -0
  66. package/dist/retrieval/models.js.map +1 -0
  67. package/dist/retrieval/queryLog.js +70 -0
  68. package/dist/retrieval/queryLog.js.map +1 -0
  69. package/dist/retrieval/refresh.js +55 -0
  70. package/dist/retrieval/refresh.js.map +1 -0
  71. package/dist/retrieval/runtime.js +161 -0
  72. package/dist/retrieval/runtime.js.map +1 -0
  73. package/dist/retrieval/search.js +129 -0
  74. package/dist/retrieval/search.js.map +1 -0
  75. package/dist/retrieval/server.js +197 -0
  76. package/dist/retrieval/server.js.map +1 -0
  77. package/dist/retrieval/session.js +41 -0
  78. package/dist/retrieval/session.js.map +1 -0
  79. package/dist/retrieval/setup.js +121 -0
  80. package/dist/retrieval/setup.js.map +1 -0
  81. package/dist/retrieval/store.js +170 -0
  82. package/dist/retrieval/store.js.map +1 -0
  83. package/package.json +22 -3
  84. package/templates/README.md +3 -2
  85. package/templates/claude/CLAUDE.md +4 -4
  86. package/templates/claude/README.md +3 -1
  87. package/templates/claude/context/conventions.md +1 -1
  88. package/templates/claude/context/layer.md +1 -1
  89. package/templates/claude/push-notify.env.example +7 -2
  90. package/templates/claude/settings.autonomous.json +1 -1
  91. package/templates/claude/settings.autonomous.retrieval.json +9 -0
  92. package/templates/github/workflows/harness-resume.yml +124 -0
  93. package/templates/github/workflows/harness-run.yml +446 -0
  94. package/templates/repo/README.md +2 -0
  95. package/templates/repo/gitignore +6 -0
  96. package/templates/repo/gitignore.retrieval +2 -0
  97. package/templates/repo/mcp.retrieval.json +11 -0
  98. package/templates/scripts/README.md +1 -1
  99. package/templates/scripts/autonomous-notify.sh +10 -4
  100. package/templates/scripts/autonomous-watcher.sh +1445 -221
  101. package/templates/scripts/cleanup-merged-worktrees.sh +126 -8
  102. package/templates/scripts/docs-search-server.sh +64 -0
  103. package/templates/scripts/flow-walker.sh +629 -0
  104. package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
  105. package/templates/scripts/lib/flow-walker-gates.sh +165 -0
  106. package/templates/scripts/lib/harness-run-lib.sh +452 -21
  107. package/templates/scripts/remote-run.sh +1785 -0
  108. package/templates/scripts/restart-watcher.sh +24 -3
  109. package/templates/scripts/run-test-suite.sh +182 -0
  110. package/templates/scripts/scratch-run.sh +2 -1
  111. package/templates/state-dir/README-root.md +1 -1
  112. package/templates/state-dir/autonomous_logs/README.md +1 -1
  113. package/templates/state-dir/business_parity_reviews/README.md +1 -1
  114. package/templates/state-dir/clarification_digests/README.md +1 -1
  115. package/templates/state-dir/clarifications/README.md +4 -4
  116. package/templates/state-dir/improvement_observations/README.md +2 -0
  117. package/templates/state-dir/scratch/README.md +1 -1
  118. package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
  119. package/templates/state-dir/test_fix_plans/README.md +9 -0
  120. package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
  121. package/templates/state-dir/test_run_logs/README.md +11 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The setup-time half of docs retrieval: installing the runtime and downloading the models, which
3
+ * `init` runs after its plan when the config in effect has retrieval on.
4
+ *
5
+ * **The rule this module exists to enforce: every download retrieval needs happens here, at setup
6
+ * time, and never on a run.** An unattended run has no network to count on, so the runtime install
7
+ * and the model download both happen while an operator is present.
8
+ *
9
+ * - **The install skips on `retrievalRuntimeState().installed` and nothing else.** The `.mcp.json`
10
+ * launcher (`docs-search-server.sh`) runs the runtime's entry alone, so an installation that
11
+ * resolves its own peers still needs the runtime. The install-state tests live in
12
+ * `retrieval/runtime.ts` alone, the same predicate `doctor` grades on.
13
+ * - **A stub run never installs and never downloads.** With `RETRIEVAL_STUB_ENV` set, a missing
14
+ * runtime is a warning rather than an `npm` call, so a test that forgets to plant one stays offline.
15
+ * - **A failed step is a warning.** The wiring the plan wrote stays valid; `doctor` fails until a
16
+ * re-run completes the step.
17
+ *
18
+ * Both steps put machine state outside the repository, not through the write engine
19
+ * (`cli/src/core/writer.ts` header). `setUpRuntime` writes the runtime itself, through `npm`;
20
+ * `setUpModels` only spawns `docs fetch-models`, and the model cache's write belongs to
21
+ * `retrieval/models.ts` → `fetchModels`, through Transformers.js, in that child process.
22
+ */
23
+ import { execFileSync } from 'node:child_process';
24
+ import { mkdirSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { ownManifestString } from '../core/paths.js';
27
+ import { modelFilesPresent, RETRIEVAL_STUB_ENV, stubModelsSelected } from './models.js';
28
+ import { retrievalCliEntry, retrievalModelCacheDir, retrievalPeers, retrievalRuntimeDir, retrievalRuntimeState, RUNTIME_CLI_RELATIVE, } from './runtime.js';
29
+ /** Both child processes may take minutes on a cold cache. */
30
+ const STEP_TIMEOUT_MS = 900000;
31
+ const REMEDY = 're-run `npx autonomous-sdlc-harness init`; `npx autonomous-sdlc-harness doctor` reports what is missing';
32
+ /** A warning for a failed child: the command, its exit status and the last line it wrote to stderr. */
33
+ function failureWarning(step, command, error) {
34
+ const failure = error;
35
+ const status = typeof failure?.status === 'number'
36
+ ? `exit status ${failure.status}`
37
+ : failure?.signal
38
+ ? `killed by ${failure.signal}`
39
+ : `not run (${failure?.code ?? String(error)})`;
40
+ const stderr = failure?.stderr === undefined ? '' : String(failure.stderr);
41
+ const lastLine = stderr
42
+ .split('\n')
43
+ .map((line) => line.trim())
44
+ .filter((line) => line !== '')
45
+ .pop();
46
+ return `docs retrieval ${step} failed: \`${command.join(' ')}\` ended with ${status}${lastLine === undefined ? '' : `: ${lastLine}`}. The retrieval wiring is written and stays valid; ${REMEDY}`;
47
+ }
48
+ function runStep(step, command, warnings) {
49
+ const [file, ...args] = command;
50
+ try {
51
+ execFileSync(file, args, { stdio: ['ignore', 'ignore', 'pipe'], timeout: STEP_TIMEOUT_MS });
52
+ }
53
+ catch (error) {
54
+ warnings.push(failureWarning(step, command, error));
55
+ }
56
+ }
57
+ function setUpRuntime(dryRun, notes, warnings) {
58
+ const runtimeDir = retrievalRuntimeDir();
59
+ const state = retrievalRuntimeState();
60
+ if (state.installed) {
61
+ notes.push(`docs retrieval runtime already installed: version ${state.version ?? 'unknown'} in ${runtimeDir}`);
62
+ return;
63
+ }
64
+ const command = [
65
+ 'npm',
66
+ 'install',
67
+ '--prefix',
68
+ runtimeDir,
69
+ '--no-audit',
70
+ '--no-fund',
71
+ `${ownManifestString('name')}@${ownManifestString('version')}`,
72
+ ...retrievalPeers().map((peer) => `${peer.name}@${peer.range}`),
73
+ ];
74
+ if (dryRun) {
75
+ notes.push(`docs retrieval runtime would be installed (dry run): \`${command.join(' ')}\``);
76
+ return;
77
+ }
78
+ if (stubModelsSelected()) {
79
+ warnings.push(`docs retrieval runtime is not installed in ${runtimeDir}, and a stub run (${RETRIEVAL_STUB_ENV} set) never installs it; unset ${RETRIEVAL_STUB_ENV} and ${REMEDY}`);
80
+ return;
81
+ }
82
+ mkdirSync(runtimeDir, { recursive: true });
83
+ runStep('runtime install', command, warnings);
84
+ }
85
+ function setUpModels(dryRun, notes, warnings) {
86
+ const cacheDir = retrievalModelCacheDir();
87
+ if (stubModelsSelected()) {
88
+ notes.push(`docs retrieval models: stub models (${RETRIEVAL_STUB_ENV} set) need no download`);
89
+ return;
90
+ }
91
+ if (modelFilesPresent(cacheDir).present) {
92
+ notes.push(`docs retrieval models already cached in ${cacheDir}`);
93
+ return;
94
+ }
95
+ // In a dry run the install above has not happened, so the runtime's entry is named where no other resolves.
96
+ const entry = retrievalCliEntry()?.entry;
97
+ const command = [
98
+ process.execPath,
99
+ entry ?? join(retrievalRuntimeDir(), RUNTIME_CLI_RELATIVE),
100
+ 'docs',
101
+ 'fetch-models',
102
+ ];
103
+ if (dryRun) {
104
+ notes.push(`docs retrieval models would be downloaded into ${cacheDir} (dry run): \`${command.join(' ')}\``);
105
+ return;
106
+ }
107
+ if (entry === undefined) {
108
+ warnings.push(`docs retrieval models were not downloaded: no CLI entry can load the retrieval packages, because the runtime in ${retrievalRuntimeDir()} is not installed; ${REMEDY}`);
109
+ return;
110
+ }
111
+ runStep('model download', command, warnings);
112
+ }
113
+ /** Install the runtime, then download the models; each step skips when already satisfied. */
114
+ export function setUpRetrieval(options) {
115
+ const notes = [];
116
+ const warnings = [];
117
+ setUpRuntime(options.dryRun, notes, warnings);
118
+ setUpModels(options.dryRun, notes, warnings);
119
+ return { notes, warnings };
120
+ }
121
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/retrieval/setup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The docs-retrieval index store: the {@link DocStore} interface every consumer takes, and its one
3
+ * implementation on an embedded Postgres (PGlite) with an HNSW vector index and a BM25 lexical index.
4
+ *
5
+ * **The rule this module exists to enforce: every value reaches SQL as a bound parameter, and the SQL
6
+ * stays plain Postgres plus the `vector` and `pg_textsearch` extensions.** A search query comes from an
7
+ * agent and is untrusted, so no input is ever spliced into a statement; the two spliced tokens are the
8
+ * `vector(<dimensions>)` column type, taken from a checked integer, and the constant index name inside
9
+ * `to_bm25query($1, 'chunks_bm25')`. Keeping the dialect plain is what would let a real Postgres
10
+ * implement {@link DocStore} by connection string; this module builds no such implementation.
11
+ *
12
+ * **The shapes the lexical plan's cost depends on are exported, and a caller composes them rather than
13
+ * retyping them:** {@link CHUNKS_TABLE}, {@link BM25_INDEX}, {@link BM25_INDEX_DEFINITION},
14
+ * {@link BM25_ORDER_CLAUSE}, {@link CHUNK_TEXT_COLUMN} and {@link chunkEmbeddingColumn}. They exist for
15
+ * `cli/test/docs-retrieval-store.test.mjs`, which measures the row count at which the planner chooses
16
+ * the BM25 index scan and must build that index *after* the rows — which this module's open path cannot
17
+ * do. The statements below are issued from those same constants, so there is no second copy inside the
18
+ * owner either, and a change to the indexed column, the `text_config`, the index name, the row width or
19
+ * the ordering operator moves the measurement with it instead of leaving it describing a store that no
20
+ * longer exists.
21
+ *
22
+ * The PGlite packages are optional peers reached only through `loadRetrievalModule`
23
+ * (`cli/src/retrieval/runtime.ts`); this file takes their types with `import type`.
24
+ */
25
+ import { mkdirSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { internal } from '../core/errors.js';
28
+ import { normalizeRepoDir } from '../core/repoPaths.js';
29
+ import { loadRetrievalModule } from './runtime.js';
30
+ /** The index directory's name under `stateDir`; the ignore rule reads it from here. */
31
+ export const INDEX_DIR_NAME = 'docs_index';
32
+ /** The chunk table's name, so a composed statement names the same table the store's own DDL creates. */
33
+ export const CHUNKS_TABLE = 'chunks';
34
+ /** The BM25 lexical index's name, spliced rather than bound — see the header's spliced-token clause. */
35
+ export const BM25_INDEX = 'chunks_bm25';
36
+ /**
37
+ * Everything after the index name in the BM25 index's `CREATE INDEX`: the indexed column and the
38
+ * `text_config` the planner's row estimate and the scan's matching-rows behaviour both depend on. The
39
+ * `IF NOT EXISTS` is the open path's alone, so a caller building the index after its rows composes the
40
+ * same definition without it.
41
+ */
42
+ export const BM25_INDEX_DEFINITION = `ON ${CHUNKS_TABLE} USING bm25 (text) WITH (text_config='english')`;
43
+ /** The lexical arm's ordering clause; `$1` is the bound query text. */
44
+ export const BM25_ORDER_CLAUSE = `text <@> to_bm25query($1, '${BM25_INDEX}')`;
45
+ /** The BM25-indexed column's declaration. */
46
+ export const CHUNK_TEXT_COLUMN = 'text text NOT NULL';
47
+ /** The embedding column's declaration; its width is what sets the rows per page a scan is costed on. */
48
+ export function chunkEmbeddingColumn(dimensions) {
49
+ return `embedding vector(${dimensions}) NOT NULL`;
50
+ }
51
+ /** `<repoRoot>/<stateDir>/docs_index` — the per-checkout index PGlite persists into. */
52
+ export function indexDataDir(repoRoot, stateDir) {
53
+ return join(repoRoot, normalizeRepoDir(stateDir), INDEX_DIR_NAME);
54
+ }
55
+ const PGLITE_SPECIFIER = '@electric-sql/pglite';
56
+ const PGVECTOR_SPECIFIER = '@electric-sql/pglite-pgvector';
57
+ const PG_TEXTSEARCH_SPECIFIER = '@electric-sql/pglite-pg_textsearch';
58
+ /** The meta key recording the width the `chunks.embedding` column was created with. */
59
+ const DIMENSIONS_META_KEY = 'dimensions';
60
+ /** The meta key refresh compares against the embedder's id; cleared when the column is recreated. */
61
+ export const EMBEDDER_META_KEY = 'embedder';
62
+ /** A vector as pgvector's text input form, bound as `$n::vector`. */
63
+ function vectorLiteral(embedding) {
64
+ return `[${embedding.map((value) => String(value)).join(',')}]`;
65
+ }
66
+ /**
67
+ * Opens the index. `dataDir: undefined` is an in-memory store; a path is created and persisted into.
68
+ *
69
+ * An existing index built at another width has its `chunks` table dropped and its embedder record
70
+ * removed, so the next refresh rebuilds rather than failing on the column type.
71
+ */
72
+ export async function openPgliteStore(options) {
73
+ const { dataDir, dimensions } = options;
74
+ if (!Number.isInteger(dimensions) || dimensions <= 0) {
75
+ throw internal(`openPgliteStore was given ${String(dimensions)} dimensions, which is not a positive integer`);
76
+ }
77
+ const { PGlite } = await loadRetrievalModule(PGLITE_SPECIFIER);
78
+ const { vector } = await loadRetrievalModule(PGVECTOR_SPECIFIER);
79
+ const { pg_textsearch } = await loadRetrievalModule(PG_TEXTSEARCH_SPECIFIER);
80
+ // The one in-repository write outside the write engine: `cli/src/core/writer.ts`'s header, the
81
+ // per-checkout docs index clause; raised for a supervised amendment in the story index's
82
+ // `## Corpus staleness`.
83
+ if (dataDir !== undefined)
84
+ mkdirSync(dataDir, { recursive: true });
85
+ const db = await PGlite.create({ dataDir, extensions: { vector, pg_textsearch } });
86
+ await db.exec('CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_textsearch;');
87
+ await db.exec('CREATE TABLE IF NOT EXISTS meta (key text PRIMARY KEY, value text NOT NULL)');
88
+ const readMeta = async (key) => {
89
+ const result = await db.query('SELECT value FROM meta WHERE key = $1', [key]);
90
+ return result.rows[0]?.value;
91
+ };
92
+ const writeMeta = async (key, value) => {
93
+ await db.query('INSERT INTO meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value', [
94
+ key,
95
+ value,
96
+ ]);
97
+ };
98
+ const width = String(dimensions);
99
+ if ((await readMeta(DIMENSIONS_META_KEY)) !== width) {
100
+ await db.exec(`DROP TABLE IF EXISTS ${CHUNKS_TABLE}`);
101
+ await db.query('DELETE FROM meta WHERE key = $1', [EMBEDDER_META_KEY]);
102
+ }
103
+ await db.exec(`CREATE TABLE IF NOT EXISTS ${CHUNKS_TABLE} (id serial PRIMARY KEY, key text UNIQUE NOT NULL, path text NOT NULL, ` +
104
+ `anchor text NOT NULL, heading text NOT NULL, body text NOT NULL, ${CHUNK_TEXT_COLUMN}, hash text NOT NULL, ` +
105
+ `${chunkEmbeddingColumn(dimensions)})`);
106
+ await db.exec(`CREATE INDEX IF NOT EXISTS chunks_hnsw ON ${CHUNKS_TABLE} USING hnsw (embedding vector_cosine_ops)`);
107
+ await db.exec(`CREATE INDEX IF NOT EXISTS ${BM25_INDEX} ${BM25_INDEX_DEFINITION}`);
108
+ await writeMeta(DIMENSIONS_META_KEY, width);
109
+ const ranked = (rows) => rows.map((row, index) => ({ id: row.id, rank: index + 1 }));
110
+ return {
111
+ readMeta,
112
+ writeMeta,
113
+ async listChunkHashes() {
114
+ const result = await db.query(`SELECT key, hash FROM ${CHUNKS_TABLE}`);
115
+ return new Map(result.rows.map((row) => [row.key, row.hash]));
116
+ },
117
+ async upsertChunks(chunks, embeddings) {
118
+ if (chunks.length !== embeddings.length) {
119
+ throw internal(`upsertChunks was given ${chunks.length} chunks and ${embeddings.length} embeddings`);
120
+ }
121
+ await db.transaction(async (tx) => {
122
+ for (const [index, chunk] of chunks.entries()) {
123
+ const embedding = embeddings[index] ?? [];
124
+ if (embedding.length !== dimensions) {
125
+ throw internal(`an embedding for ${chunk.key} has ${embedding.length} dimensions where the index has ${dimensions}`);
126
+ }
127
+ await tx.query(`INSERT INTO ${CHUNKS_TABLE} (key, path, anchor, heading, body, text, hash, embedding) ` +
128
+ 'VALUES ($1, $2, $3, $4, $5, $6, $7, $8::vector) ' +
129
+ 'ON CONFLICT (key) DO UPDATE SET path = EXCLUDED.path, anchor = EXCLUDED.anchor, heading = EXCLUDED.heading, ' +
130
+ 'body = EXCLUDED.body, text = EXCLUDED.text, hash = EXCLUDED.hash, embedding = EXCLUDED.embedding', [chunk.key, chunk.path, chunk.anchor, chunk.heading, chunk.body, chunk.text, chunk.hash, vectorLiteral(embedding)]);
131
+ }
132
+ });
133
+ },
134
+ async deleteChunks(keys) {
135
+ if (keys.length === 0)
136
+ return;
137
+ await db.query(`DELETE FROM ${CHUNKS_TABLE} WHERE key = ANY($1::text[])`, [[...keys]]);
138
+ },
139
+ async clear() {
140
+ await db.exec(`DELETE FROM ${CHUNKS_TABLE}`);
141
+ },
142
+ async lexicalSearch(query, limit) {
143
+ if (query.trim() === '')
144
+ return [];
145
+ // pg_textsearch scores lower-is-better; a non-matching row scores 0, so matches sort first.
146
+ // Filtering to matching rows alone is the index scan's behaviour, not the operator's — see the
147
+ // interface comment above.
148
+ const result = await db.query(`SELECT id FROM ${CHUNKS_TABLE} ORDER BY ${BM25_ORDER_CLAUSE} LIMIT $2`, [query, limit]);
149
+ return ranked(result.rows);
150
+ },
151
+ async vectorSearch(embedding, limit) {
152
+ const result = await db.query(`SELECT id FROM ${CHUNKS_TABLE} ORDER BY embedding <=> $1::vector LIMIT $2`, [vectorLiteral(embedding), limit]);
153
+ return ranked(result.rows);
154
+ },
155
+ async getChunks(ids) {
156
+ if (ids.length === 0)
157
+ return [];
158
+ const result = await db.query(`SELECT id, key, path, anchor, heading, body FROM ${CHUNKS_TABLE} WHERE id = ANY($1::int[])`, [[...ids]]);
159
+ const byId = new Map(result.rows.map((row) => [row.id, row]));
160
+ return ids.flatMap((id) => {
161
+ const row = byId.get(id);
162
+ return row === undefined ? [] : [row];
163
+ });
164
+ },
165
+ async close() {
166
+ await db.close();
167
+ },
168
+ };
169
+ }
170
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/retrieval/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "autonomous-sdlc-harness",
3
- "version": "0.1.0",
4
- "description": "The outer loop of the autonomous SDLC harness: init, doctor, config and daemon management for the harness Claude Code plugin.",
3
+ "version": "0.4.0",
4
+ "description": "The outer loop of the autonomous SDLC harness: init, doctor, config and daemon management for the harness Claude Code plugin, plus opt-in local docs retrieval.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "firu-daniel",
7
7
  "homepage": "https://github.com/firu-daniel/autonomous-sdlc-harness",
@@ -16,8 +16,27 @@
16
16
  "test": "node --test",
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
+ "peerDependencies": {
20
+ "@electric-sql/pglite": "0.5.8",
21
+ "@electric-sql/pglite-pg_textsearch": "^0.0.10",
22
+ "@electric-sql/pglite-pgvector": "^0.0.9",
23
+ "@huggingface/transformers": "^4.3.0",
24
+ "@modelcontextprotocol/sdk": "^1.30.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "@electric-sql/pglite": { "optional": true },
28
+ "@electric-sql/pglite-pg_textsearch": { "optional": true },
29
+ "@electric-sql/pglite-pgvector": { "optional": true },
30
+ "@huggingface/transformers": { "optional": true },
31
+ "@modelcontextprotocol/sdk": { "optional": true }
32
+ },
19
33
  "devDependencies": {
20
34
  "typescript": "^5.6.0",
21
- "@types/node": "^20.14.0"
35
+ "@types/node": "^20.14.0",
36
+ "@electric-sql/pglite": "0.5.8",
37
+ "@electric-sql/pglite-pg_textsearch": "^0.0.10",
38
+ "@electric-sql/pglite-pgvector": "^0.0.9",
39
+ "@huggingface/transformers": "^4.3.0",
40
+ "@modelcontextprotocol/sdk": "^1.30.0"
22
41
  }
23
42
  }
@@ -9,7 +9,8 @@ The generator templates `init` copies into an adopted repository. Nothing here i
9
9
  | `githooks/` | the configured `githooksDir` |
10
10
  | `scripts/` | the configured `scriptsDir` |
11
11
  | `state-dir/` | the configured `stateDir` |
12
+ | `github/` | `.github/` in the adopter's repo — `workflows/harness-run.yml` and `workflows/harness-resume.yml`, written only when `execution.target` is `github-actions` |
12
13
 
13
- **Naming rule for the whole tree: a template whose adopter-side name begins with a dot is stored here without the dot.** `repo/gitignore` is written as `.gitignore`, `repo/mcp.json` as `.mcp.json`, and the `claude/` subtree lands at `.claude/`; `init` adds the dot when it writes. The rule is load-bearing twice over — a real `.gitignore` inside this repository would be honoured by git and could silently exclude files that are meant to ship, and npm rewrites a packaged `.gitignore` to `.npmignore` on publish, so a dot-named template does not survive the wire.
14
+ **Naming rule for the whole tree: a template whose adopter-side name begins with a dot is stored here without the dot.** `repo/gitignore` is written as `.gitignore`, `repo/mcp.json` as `.mcp.json`, and the `claude/` subtree lands at `.claude/` and the `github/` subtree at `.github/`; `init` adds the dot when it writes. The rule is load-bearing twice over — a real `.gitignore` inside this repository would be honoured by git and could silently exclude files that are meant to ship, and npm rewrites a packaged `.gitignore` to `.npmignore` on publish, so a dot-named template does not survive the wire.
14
15
 
15
- A template whose adopter-side home is none of the five above is new information for the item that adds it: give it a sixth subdirectory rather than forcing it into one of these.
16
+ A template whose adopter-side home is none of the six above is new information for the item that adds it: give it a seventh subdirectory rather than forcing it into one of these.
@@ -2,7 +2,7 @@
2
2
 
3
3
  {{setupBanner}}
4
4
 
5
- _`/harness-analyze` writes this paragraph from the repository itself — or replace this line by hand: what this project is, who uses it, and anything an agent must know before it touches a file._
5
+ _`/{{pluginName}}:harness-analyze` writes this paragraph from the repository itself — or replace this line by hand: what this project is, who uses it, and anything an agent must know before it touches a file._
6
6
 
7
7
  ---
8
8
 
@@ -12,7 +12,7 @@ _`/harness-analyze` writes this paragraph from the repository itself — or repl
12
12
  |---|---|---|
13
13
  | _(kind of file)_ | _(the name pattern it follows)_ | _(one real file in this repository that follows it)_ |
14
14
 
15
- _`/harness-analyze` fills this table from the repository's real file names — or add the rows by hand, one per kind of file this project has a naming rule for. Until it is filled, an implementer follows whatever the files around it already do, which is slower and less consistent than one row here._
15
+ _`/{{pluginName}}:harness-analyze` fills this table from the repository's real file names — or add the rows by hand, one per kind of file this project has a naming rule for. Until it is filled, an implementer follows whatever the files around it already do, which is slower and less consistent than one row here._
16
16
 
17
17
  ---
18
18
 
@@ -26,7 +26,7 @@ This file is auto-loaded into every agent, which is why it deliberately holds al
26
26
 
27
27
  > **Checkout root — derive it, never assume it.** Take the root of the checkout **you are running in** from a **bare** `git rev-parse --show-toplevel` (a read-only command an unattended run can allow-list as a literal), then build every literal path from that result — this run's artifacts under `<root>/{{stateDir}}/`, the lessons ledger at `<root>/{{stateDir}}/lessons.md`. This binds **every** repo-relative path you are given and every file you write, not only the ledger. Do not hardcode an absolute path, do not wrap the substitution inside another shell command, and do not stash it in a shell variable — separate tool calls do not share shell state, and a command carrying a substitution is not reliably auto-allowed in an unattended run. In a second working copy the original checkout is a *different branch*: reading it returns that branch's file, and writing to it puts this run's artifact on the wrong branch. One exception, and it is not one you resolve: an **argument** path handed to a wrapper script is **relative to that wrapper's own base and never prefixed with a checkout root** — which base that is, is the wrapper's to state (its `--repo`, or the directory it `cd`s to); take it from the wrapper's own documented usage rather than assuming the repo top. The wrapper's own invocation path is rooted like everything else. The links in the table above are document pointers; resolve the live path this way at read and write time.
28
28
 
29
- The rows above were generated from the `layers` list in `harness.config.json`, so this table and the layers the orchestrator dispatches on started out in agreement. Keep them that way: add a layer there, then add its row here by hand — that is the route that costs nothing. `autonomous-sdlc-harness init --force` will also re-render the rows from the `layers` list as it then stands (`--force` overwrites generated files but never `harness.config.json`, which `autonomous-sdlc-harness init` reads on every run, so the layer just added is the one the rows come from), but it regenerates **this whole file** from the template — every section `/harness-analyze` filled goes with it, recoverable only from the single `CLAUDE.md.bak` the run writes. That `.bak` is single-generation, and the next `--force` does not necessarily spend it: a forced run that finds this file byte-identical to the one it would write keeps the file and leaves the `.bak` alone, so the sections a previous pass rescued into it stay there. What overwrites a `.bak` holding filled sections is a forced run over a file that has been filled again since. The orchestrator reads none of these files; the committer reads exactly one section of one of them — the commit-message policy in the shared cross-layer conventions document — and nothing else. One read-on-demand document is deliberately not in that table because no layer owns it: `.claude/harness-task-offer.md`, which `## Where a change request runs` below points at directly and nothing else reads.
29
+ The rows above were generated from the `layers` list in `harness.config.json`, so this table and the layers the orchestrator dispatches on started out in agreement. Keep them that way: add a layer there, then add its row here by hand — that is the route that costs nothing. `autonomous-sdlc-harness init --force` will also re-render the rows from the `layers` list as it then stands (`--force` overwrites generated files but never `harness.config.json`, which `autonomous-sdlc-harness init` reads on every run, so the layer just added is the one the rows come from), but it regenerates **this whole file** from the template — every section `/{{pluginName}}:harness-analyze` filled goes with it, recoverable only from the single `CLAUDE.md.bak` the run writes. That `.bak` is single-generation, and the next `--force` does not necessarily spend it: a forced run that finds this file byte-identical to the one it would write keeps the file and leaves the `.bak` alone, so the sections a previous pass rescued into it stay there. What overwrites a `.bak` holding filled sections is a forced run over a file that has been filled again since. The orchestrator reads none of these files; the committer reads exactly one section of one of them — the commit-message policy in the shared cross-layer conventions document — and nothing else. One read-on-demand document is deliberately not in that table because no layer owns it: `.claude/harness-task-offer.md`, which `## Where a change request runs` below points at directly and nothing else reads.
30
30
 
31
31
  ---
32
32
 
@@ -43,7 +43,7 @@ The allowlist is the whole mechanism, deliberately: a global deny is evaluated b
43
43
  Four tests, all of which must hold, **in this order** — the first three cost nothing, so reach the fourth only when they have all passed. If any fails: say nothing, offer nothing, and carry out the instruction you were given.
44
44
 
45
45
  1. **Tool.** `AskUserQuestion` is in your own toolset. If it is not, you are an unattended run and nothing below applies to you.
46
- 2. **Provenance.** The message was **typed by the user in this conversation**. Not an instruction you are executing from a harness command or a flow-instruction document it dispatched — every `/branch-*` and `/harness-*`, supervised, semi-autonomous and unattended alike, including reading a task prompt or a review file as your own work. Not one handed to you as a **dispatched sub-agent**, whichever agent type you are and whatever your toolset holds. Not a continuation of work the user has already routed, in this conversation or in the one that dispatched you.
46
+ 2. **Provenance.** The message was **typed by the user in this conversation**. Not an instruction you are executing from a harness command or a flow-instruction document it dispatched — every `/{{pluginName}}:branch-*` and `/{{pluginName}}:harness-*`, supervised, semi-autonomous and unattended alike, including reading a task prompt or a review file as your own work. Not one handed to you as a **dispatched sub-agent**, whichever agent type you are and whatever your toolset holds. Not a continuation of work the user has already routed, in this conversation or in the one that dispatched you.
47
47
  3. **Trigger.** The message **asks for a change to this project's code** — a feature, a fix, a refactor, a chore. **Size is never a factor.** The line is *asks for a change* versus *asks about the code*: *"explain this function"* fires **nothing**; *"this button isn't centred"* fires; *"check this file `/some/path/notes.txt` to implement adding comments to a content item"* fires too, because where a spec lives does not change what is being asked. A message invoking or continuing a harness command does not fire. Any shape you cannot place: **stay silent**.
48
48
  4. **Opt-out.** `.claude/harness-no-offer` does not exist at the **main worktree's** root — the checkout you are running in may be a worktree of it, and the marker is written once for all of them. Test it with the shell rather than by reading anything: `test -e "$(git worktree list | head -1 | awk '{print $1}')/.claude/harness-no-offer"`, whose first line is always the main worktree and which is the same path in an ordinary single-checkout repository; the checkout-root rule's literal-command constraint is an unattended-run allow-listing concern and clause 1 has already excluded those. Presence-only: never read, parse or act on anything inside it — a read of a path outside this session's own root can raise a permission prompt mid-fence, while the test's non-zero exit is a clean answer. The file is normally absent, and absent means only *not opted out*; a check you cannot make — the command declined, the repository not a git one, no output — is silence too, never a remark to the user about a file they never created.
49
49
 
@@ -1,5 +1,7 @@
1
1
  # claude/
2
2
 
3
- Copied into the adopter's `.claude/` directory by `autonomous-sdlc-harness init`: the project-context and conventions stubs that `/harness-analyze` fills in from the adopted repository's real code, under the marker contract `docs/analyze.md` records, the settings and permission profile the unattended modes run under (roadmap item 14), the `*.env.example` files an adopter copies and completes with its own credentials and push configuration, `harness-task-offer.md`, the change-request offer rules the always-loaded file's fence reads on demand, and — when the interactive test phase is on — the test-scenario-rules skeleton that phase's agents read. The settings profile is the reason a CLI exists at all — a plugin cannot write a repository's `settings.json`, and getting that profile right, with every wrapper script allow-listed in the exact literal form the guard matches, is the highest-friction part of adoption.
3
+ Copied into the adopter's `.claude/` directory by `autonomous-sdlc-harness init`: the project-context and conventions stubs that `/autonomous-sdlc-harness:harness-analyze` fills in from the adopted repository's real code, under the marker contract `docs/analyze.md` records, the settings and permission profile the unattended modes run under (roadmap item 14), the `*.env.example` files an adopter copies and completes with its own credentials and push configuration, `harness-task-offer.md`, the change-request offer rules the always-loaded file's fence reads on demand, and — when the interactive test phase is on — the test-scenario-rules skeleton that phase's agents read. The settings profile is the reason a CLI exists at all — a plugin cannot write a repository's `settings.json`, and getting that profile right, with every wrapper script allow-listed in the exact literal form the guard matches, is the highest-friction part of adoption.
4
+
5
+ `settings.autonomous.qa.json` and `settings.autonomous.retrieval.json` are **fragments** merged into that profile rather than second profiles: the first when the interactive test phase drives a browser, the second when `phases.docs` and `docs.retrieval` are both on. The retrieval fragment starts the `harness-docs` server `repo/mcp.retrieval.json` declares and allows its one tool, and needs both keys: a server that is not started has no tools, and an un-loaded tool stalls an unattended run.
4
6
 
5
7
  **This directory is never named `.claude` inside this repository, and must not be renamed to it.** These are templates for **someone else's** configuration, not this repository's own: the dot is added at the adopter's end, by `autonomous-sdlc-harness init`, and nowhere else, so the stored path is the undotted one. The reason once given here was that a dotted path would register these templates as live agents and commands in any session opened at this root; that was measured false on Claude Code 2.1.234 on 2026-08-19, against a root-level control in the same repository — a `.claude/` in a subdirectory registers nothing into a session rooted above it — so do not restore it. The one literal `.claude/` in this tree, under `examples/notes-app/`, is an adopted repository's own generated output rather than a template, which is why it is not a counter-example to this rule.
@@ -10,7 +10,7 @@
10
10
  - How a change flows through those layers, and which direction a dependency is allowed to point.
11
11
  - The order files are created in when a feature is added, so a half-built feature is still a coherent tree.
12
12
  - Where a new responsibility goes when it fits no file that already exists.
13
- - Logging, error handling, and the testing bar a change clears before it counts as done.
13
+ - Logging, error handling, and the testing bar a change clears before it counts as done — including **the one command that runs a single test file on its own**, or the statement that this project has none. The harness runs the full test command once per Run gates phase, never per change, so a unit that wrote a test runs only that file, through this command. Where the test wrapper's line accepts a path, `bash <scriptsDir>/test.sh <file>` is the command to name, because it is already allow-listed.
14
14
  - **The commit-message policy**, which is the whole of what the committer agent reads from this document: the subject-prefix vocabulary a caller draws its prefix from, whether the first word after the prefix is capitalized, the subject-length cap, the fixed-form subjects the flow emits, and the attribution-trailer policy: whether a trailer naming the agent as an author is required, forbidden or optional. The harness takes no position on that trailer, because the rule is actively reversed between projects: some forbid one outright, others require it on every agent-assisted commit, and unanswered here it has no default to fall back on. **The fixed-form subjects are derived here, never copied from a list** — run ``grep -rnE '(chore|feat|fix|refactor|docs|test): [A-Za-z][^"'"'"'`]*(<branch>|\$branch|\$\{branch\}|<entry-id>|K passing)' "${CLAUDE_PLUGIN_ROOT}" <scripts_dir>``, substituting `<scripts_dir>` with this repository's `scriptsDir` value from `harness.config.json` before running it, and write the policy so that **every** subject that command emits is exempt from the capitalisation rule and from the subject-length cap — **minus two classes the probe reaches but no commit point owns**: a subject the surrounding text tells the flow **not** to emit (its own line says it is forbidden), and a subject quoted as a worked example by a guard, a verification document or a prose restatement of another home's string. A subject with no emitting home is not a fixed form; check the line the probe emitted before exempting it. Each exempted subject is passed byte-for-byte by the commit point that owns it, so a policy that re-cases or truncates one breaks that commit point. `review_plan_file` and `ui_test_pass` are the committer's own commit modes and the dispatching flow files quote both, so changing either one is a two-file edit; the flow's other commit points own theirs the same way. The probe reaches **branch-scoped** subjects only — its alternation requires a `<branch>` / `$branch` / `${branch}` / `<entry-id>` / `K passing` token — so a fixed subject carrying none of them falls outside it, and widening the probe is part of the cost of adding one. **Per commit class, which prefix a commit of that class takes** — the vocabulary above names the set, this names the selection, and a policy giving only the set leaves a caller nothing to read: answer it for every class the flows commit in, at minimum a commit **fixing existing work**, the class the review-fix loops commit in, and a commit **adding new work**, the class a story task's commit falls in; and answer as well whether **any class carries no prefix at all, and which class that is**, which is what the committer's `commit_prefix` argument spells as the literal `none`. Designate by class and never by file kind — the classes are the flows', the tokens are yours.
15
15
  - **How this project stays in step with its reference implementation, if it has one** — which behaviour must match it exactly, which parts may deviate and why, and how a review finding cites the source it compared against. A project with a single implementation says so here and leaves `phases.parity` off; the section is optional, not missing.
16
16
 
@@ -9,7 +9,7 @@
9
9
  - What this layer is responsible for and — just as usefully — what it is not, so a task that has drifted into it is recognisable.
10
10
  - What it may depend on, and which direction a dependency between layers is allowed to point.
11
11
  - Naming and file-layout rules that apply inside it.
12
- - What "done" means here: the tests to write, the checks to run, the bar a review holds the change to.
12
+ - What "done" means here: the tests to write, the checks to run, the bar a review holds the change to — including **the one command that runs a single test file of this layer on its own**, or the statement that this layer has none. The harness runs the full test command once per Run gates phase, never per change, so a unit that wrote a test runs only that file, through this command. Where the test wrapper's line accepts a path, `bash <scriptsDir>/test.sh <file>` is the command to name, because it is already allow-listed.
13
13
 
14
14
  **One generic example — the shape a rule takes here**
15
15
 
@@ -12,8 +12,13 @@
12
12
  # carrying settings over from an earlier runner, rename the keys — the reader recognises
13
13
  # these spellings and no others.
14
14
 
15
- # Where a run posts its notifications: any endpoint that accepts an HTTP POST.
16
- # Leave it empty to send none.
15
+ # Where a run posts its notifications: the full http:// or https:// URL of any endpoint
16
+ # that accepts an HTTP POST. Leave it empty to send none.
17
+ # For ntfy with no server of your own: install the ntfy app, create a topic there and
18
+ # write https://ntfy.sh/<your-topic> here. The topic name works like a password —
19
+ # anyone who knows it can read and send these notifications.
20
+ # This file is sourced by a shell, so a URL containing shell characters such as `&`
21
+ # must be single-quoted: HARNESS_PUSH_URL='https://…?a=1&b=2'.
17
22
  HARNESS_PUSH_URL=
18
23
 
19
24
  # Optional. A local command run in addition to the POST — a desktop notifier, a chat
@@ -7,7 +7,7 @@
7
7
  "ONE COMMAND PER ENTRY, NEVER A COMPOUND: a single `mkdir && cp && rm` block is refused where each of those commands succeeds on its own, because the joined string matches no entry. Keep every entry a single command.",
8
8
  "THE CHECKOUT AND ITS SIBLING WORKTREES BOTH: every file glob and every wrapper path is listed for {{repoRoot}} and for the sibling pattern {{worktreeGlob}} under {{workRoot}}. A run in a second working copy that matches neither does not fail loudly - it silently skips whatever phase needed the path. A single-checkout repository simply has no sibling for the second entry to match, which costs nothing.",
9
9
  "EVERY WRAPPER IN {{scriptsDir}} IS LISTED IN ALL THREE FORMS A CALLER MAY USE: the repo-relative invocation `commands.*` holds and an agent runs verbatim from the repository root, its repo-root-absolute twin for a caller that resolves the path first, and its sibling-worktree twin. Listing only one form leaves the other two matching neither `allow` nor `deny`, which is the stall this profile exists to prevent. THE DEPLOY WRAPPER IS ONE OF THEM, SO ITS THREE FORMS ARE IN `allow` HERE AND AGAIN IN `ask` BELOW: `ask` is evaluated before `allow` and returns first, so the ask is what a deploy hits and the allow rows never decide it. Deleting the `ask` deploy rows as duplicates therefore leaves a deploy an unattended run makes on its own - remove deploy from both blocks or from neither.",
10
- "THE OUTER-LOOP SCRIPTS AN AGENT RUNS ARE LISTED THE SAME THREE WAYS, AND THE REST GET NOTHING: `autonomous-sdlc-harness init` writes a second family of scripts into {{scriptsDir}} whose bodies ship fixed and read `harness.config.json` at run time rather than carrying a command line of yours. Only the ones a dispatched agent is itself the thing that runs are listed here, and they are in `allow` rather than `ask` because they sit on the unattended commit path, where an `ask` is evaluated first and parks the run with nothing to answer it. The run watcher, the worktree tooling and the cleanup sweep are started by the watcher process or by you, and they carry no entry on purpose: one would hand a dispatched agent a path to a branch deletion or a daemon restart. Adding an entry for one of them is a decision to let an agent run it - make it deliberately, in the same three forms as everything else here.",
10
+ "THE OUTER-LOOP SCRIPTS AN AGENT RUNS ARE LISTED THE SAME THREE WAYS, AND THE REST GET NOTHING: `autonomous-sdlc-harness init` writes a second family of scripts into {{scriptsDir}} whose bodies ship fixed and read `harness.config.json` at run time rather than carrying a command line of yours. Only the ones a dispatched agent or the orchestrating session is itself the thing that runs are listed here - the orchestrating session runs two of them, the flow walker `flow-walker.sh` and the test-suite runner `run-test-suite.sh` - and they are in `allow` rather than `ask` because they sit on the unattended commit, planning or gate-run path, where an `ask` is evaluated first and parks the run with nothing to answer it. The run watcher, the worktree tooling and the cleanup sweep are started by the watcher process or by you, and `remote-run.sh` by the watcher, the remote job or you; they carry no entry on purpose: one would hand a dispatched agent a path to a branch deletion, a daemon restart or a remote run dispatch. The docs-retrieval server launcher carries no entry either, for a different reason: the agent runner starts it from `.mcp.json` when docs.retrieval is on, never a dispatched agent's Bash call, and the one tool it serves, `search_docs` on the `harness-docs` server, is granted by the retrieval fragment's entry for it instead. Adding an entry for one of them is a decision to let an agent run it - make it deliberately, in the same three forms as everything else here.",
11
11
  "THE PACKAGE MANAGER, THE BUILD AND THE DEPENDENCY INSTALL ARE DELIBERATELY NOT ALLOW-LISTED: `commands.build` and `commands.depInstall` are raw command lines with no wrapper, because they are run by a person or by the outer loop rather than by an agent, and granting an agent a package manager grants it every script that package manager can run. An adopter who wants one of them agent-run adds a wrapper script for it and one literal entry here, in the same three forms as the wrappers above. That stance leaves an ad-hoc probe a route, and it needs no entry here either - the next paragraph is that route, and the trade it makes rather than an exception it satisfies.",
12
12
  "THE PROBE ROUTE GRANTS AN INTERPRETER AT ONE REMOVE, AND IT IS BROADER THAN THE GRANT ABOVE REFUSES, NOT NARROWER: an ad-hoc probe or a mutation check is written into the run-artifact tree's `scratch/` directory and run through `bash {{scriptsDir}}/scratch-run.sh`, an outer-loop script whose three forms are already listed above. It runs a FILE from that one directory rather than a string on the command line, which fences WHICH FILE runs and nothing about what that file does: the file is one the agent composed a tool call earlier, it runs with the session's own privileges, and it reaches everything the `deny` floor below, the `ask` list below and the plugin guard's own deny list withhold from a COMMAND STRING. The trade is deliberate and is recorded here rather than left to be discovered: the alternative is a run with no permitted way to execute anything, which silently downgrades a verification claim from executed to reasoned. What still holds for a subprocess is the `pre-push` git hook, because git runs it however git was invoked. Removing the route means deleting `scratch-run.sh`'s `agentInvocable` row in the generator and adding its basename to the plugin guard's deny list - both, since either alone leaves it reachable.",
13
13
  "CREATING A FILE AND CHANGING ONE ARE TWO DIFFERENT GRANTS: `Edit` covers a change to a file that already exists, `Write` covers creating one - and almost every artifact a run produces is a new file, from a plan to a review to a ledger entry to whatever source file an implementer adds. Dropping the `Write` pair therefore leaves the most common operation in the whole flow matching neither `allow` nor `deny`, which in print mode is a stall and not a refusal. `defaultMode` does not close that gap: its auto-accept is scoped to the session's own working directory, and the entry that matters most here is the sibling-worktree one.",
@@ -0,0 +1,9 @@
1
+ {
2
+ "_README": [
3
+ "THE DOCS-RETRIEVAL HALF, ADDED ONLY WHEN TWO CONDITIONS BOTH HOLD - THE DOCS PHASE IS ENABLED, AND DOCS RETRIEVAL IS ON: the `harness-docs` entry in `enabledMcpjsonServers` and the one `mcp__harness-docs__` entry at the end of `allow` were merged in because `phases.docs` AND `docs.retrieval` were both true when this file was generated. Fail either condition and neither is written. BOTH ARE NEEDED AND THEY DO DIFFERENT THINGS: the enablement STARTS the server the repository's own `.mcp.json` declares, and the allow entry lets its one tool run - without the enablement the tool does not exist, and an un-loaded tool in print mode stalls the run rather than failing it. Turning retrieval on in `harness.config.json` and re-running `autonomous-sdlc-harness init` does NOT rewrite this file - it is yours once written, and a plain re-run adds the server to `.mcp.json` while leaving this profile starting none of it; run `init --force`, which takes a .bak first, or copy the two entries in from a fresh generation."
4
+ ],
5
+ "enabledMcpjsonServers": ["harness-docs"],
6
+ "permissions": {
7
+ "allow": ["mcp__harness-docs__search_docs"]
8
+ }
9
+ }