autonomous-sdlc-harness 0.1.0 → 0.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.
- package/README.md +4 -3
- package/dist/commands/docs.js +219 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +138 -44
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/registry.js.map +1 -1
- package/dist/config/check.js +25 -5
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +18 -1
- package/dist/config/model.js.map +1 -1
- package/dist/core/layerGapRemedy.js +3 -2
- package/dist/core/layerGapRemedy.js.map +1 -1
- package/dist/core/pluginIdentity.js +33 -0
- package/dist/core/pluginIdentity.js.map +1 -0
- package/dist/core/report.js +9 -0
- package/dist/core/report.js.map +1 -1
- package/dist/core/writer.js +22 -5
- package/dist/core/writer.js.map +1 -1
- package/dist/detect/presets.js +35 -26
- package/dist/detect/presets.js.map +1 -1
- package/dist/detect/signals.js +13 -9
- package/dist/detect/signals.js.map +1 -1
- package/dist/doctor/checks.js +141 -11
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/claudeContext.js +4 -5
- package/dist/generators/claudeContext.js.map +1 -1
- package/dist/generators/harnessConfig.js +13 -5
- package/dist/generators/harnessConfig.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +13 -0
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +51 -9
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/projectSettings.js +5 -15
- package/dist/generators/projectSettings.js.map +1 -1
- package/dist/generators/repoRoot.js +130 -23
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/scripts.js +4 -1
- package/dist/generators/scripts.js.map +1 -1
- package/dist/machine/paths.js +16 -4
- package/dist/machine/paths.js.map +1 -1
- package/dist/machine/plugins.js +2 -1
- package/dist/machine/plugins.js.map +1 -1
- package/dist/retrieval/chunk.js +158 -0
- package/dist/retrieval/chunk.js.map +1 -0
- package/dist/retrieval/corpus.js +75 -0
- package/dist/retrieval/corpus.js.map +1 -0
- package/dist/retrieval/models.js +175 -0
- package/dist/retrieval/models.js.map +1 -0
- package/dist/retrieval/queryLog.js +68 -0
- package/dist/retrieval/queryLog.js.map +1 -0
- package/dist/retrieval/refresh.js +55 -0
- package/dist/retrieval/refresh.js.map +1 -0
- package/dist/retrieval/runtime.js +171 -0
- package/dist/retrieval/runtime.js.map +1 -0
- package/dist/retrieval/search.js +118 -0
- package/dist/retrieval/search.js.map +1 -0
- package/dist/retrieval/server.js +197 -0
- package/dist/retrieval/server.js.map +1 -0
- package/dist/retrieval/session.js +41 -0
- package/dist/retrieval/session.js.map +1 -0
- package/dist/retrieval/setup.js +120 -0
- package/dist/retrieval/setup.js.map +1 -0
- package/dist/retrieval/store.js +170 -0
- package/dist/retrieval/store.js.map +1 -0
- package/package.json +22 -3
- package/templates/claude/CLAUDE.md +4 -4
- package/templates/claude/README.md +3 -1
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/claude/settings.autonomous.retrieval.json +9 -0
- package/templates/repo/README.md +2 -0
- package/templates/repo/gitignore +1 -0
- package/templates/repo/gitignore.retrieval +2 -0
- package/templates/repo/mcp.retrieval.json +11 -0
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-notify.sh +10 -4
- package/templates/scripts/autonomous-watcher.sh +336 -110
- package/templates/scripts/cleanup-merged-worktrees.sh +126 -8
- package/templates/scripts/docs-search-server.sh +64 -0
- package/templates/scripts/lib/harness-run-lib.sh +19 -3
- package/templates/scripts/restart-watcher.sh +4 -3
- package/templates/state-dir/business_parity_reviews/README.md +1 -1
- package/templates/state-dir/clarification_digests/README.md +1 -1
- package/templates/state-dir/clarifications/README.md +4 -4
|
@@ -0,0 +1,120 @@
|
|
|
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 { modelFilesPresent, RETRIEVAL_STUB_ENV, stubModelsSelected } from './models.js';
|
|
27
|
+
import { ownManifestString, retrievalCliEntry, retrievalModelCacheDir, retrievalPeers, retrievalRuntimeDir, retrievalRuntimeState, RUNTIME_CLI_RELATIVE, } from './runtime.js';
|
|
28
|
+
/** Both child processes may take minutes on a cold cache. */
|
|
29
|
+
const STEP_TIMEOUT_MS = 900000;
|
|
30
|
+
const REMEDY = 're-run `npx autonomous-sdlc-harness init`; `npx autonomous-sdlc-harness doctor` reports what is missing';
|
|
31
|
+
/** A warning for a failed child: the command, its exit status and the last line it wrote to stderr. */
|
|
32
|
+
function failureWarning(step, command, error) {
|
|
33
|
+
const failure = error;
|
|
34
|
+
const status = typeof failure?.status === 'number'
|
|
35
|
+
? `exit status ${failure.status}`
|
|
36
|
+
: failure?.signal
|
|
37
|
+
? `killed by ${failure.signal}`
|
|
38
|
+
: `not run (${failure?.code ?? String(error)})`;
|
|
39
|
+
const stderr = failure?.stderr === undefined ? '' : String(failure.stderr);
|
|
40
|
+
const lastLine = stderr
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => line.trim())
|
|
43
|
+
.filter((line) => line !== '')
|
|
44
|
+
.pop();
|
|
45
|
+
return `docs retrieval ${step} failed: \`${command.join(' ')}\` ended with ${status}${lastLine === undefined ? '' : `: ${lastLine}`}. The retrieval wiring is written and stays valid; ${REMEDY}`;
|
|
46
|
+
}
|
|
47
|
+
function runStep(step, command, warnings) {
|
|
48
|
+
const [file, ...args] = command;
|
|
49
|
+
try {
|
|
50
|
+
execFileSync(file, args, { stdio: ['ignore', 'ignore', 'pipe'], timeout: STEP_TIMEOUT_MS });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
warnings.push(failureWarning(step, command, error));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function setUpRuntime(dryRun, notes, warnings) {
|
|
57
|
+
const runtimeDir = retrievalRuntimeDir();
|
|
58
|
+
const state = retrievalRuntimeState();
|
|
59
|
+
if (state.installed) {
|
|
60
|
+
notes.push(`docs retrieval runtime already installed: version ${state.version ?? 'unknown'} in ${runtimeDir}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const command = [
|
|
64
|
+
'npm',
|
|
65
|
+
'install',
|
|
66
|
+
'--prefix',
|
|
67
|
+
runtimeDir,
|
|
68
|
+
'--no-audit',
|
|
69
|
+
'--no-fund',
|
|
70
|
+
`${ownManifestString('name')}@${ownManifestString('version')}`,
|
|
71
|
+
...retrievalPeers().map((peer) => `${peer.name}@${peer.range}`),
|
|
72
|
+
];
|
|
73
|
+
if (dryRun) {
|
|
74
|
+
notes.push(`docs retrieval runtime would be installed (dry run): \`${command.join(' ')}\``);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (stubModelsSelected()) {
|
|
78
|
+
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}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
82
|
+
runStep('runtime install', command, warnings);
|
|
83
|
+
}
|
|
84
|
+
function setUpModels(dryRun, notes, warnings) {
|
|
85
|
+
const cacheDir = retrievalModelCacheDir();
|
|
86
|
+
if (stubModelsSelected()) {
|
|
87
|
+
notes.push(`docs retrieval models: stub models (${RETRIEVAL_STUB_ENV} set) need no download`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (modelFilesPresent(cacheDir).present) {
|
|
91
|
+
notes.push(`docs retrieval models already cached in ${cacheDir}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// In a dry run the install above has not happened, so the runtime's entry is named where no other resolves.
|
|
95
|
+
const entry = retrievalCliEntry()?.entry;
|
|
96
|
+
const command = [
|
|
97
|
+
process.execPath,
|
|
98
|
+
entry ?? join(retrievalRuntimeDir(), RUNTIME_CLI_RELATIVE),
|
|
99
|
+
'docs',
|
|
100
|
+
'fetch-models',
|
|
101
|
+
];
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
notes.push(`docs retrieval models would be downloaded into ${cacheDir} (dry run): \`${command.join(' ')}\``);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (entry === undefined) {
|
|
107
|
+
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}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
runStep('model download', command, warnings);
|
|
111
|
+
}
|
|
112
|
+
/** Install the runtime, then download the models; each step skips when already satisfied. */
|
|
113
|
+
export function setUpRetrieval(options) {
|
|
114
|
+
const notes = [];
|
|
115
|
+
const warnings = [];
|
|
116
|
+
setUpRuntime(options.dryRun, notes, warnings);
|
|
117
|
+
setUpModels(options.dryRun, notes, warnings);
|
|
118
|
+
return { notes, warnings };
|
|
119
|
+
}
|
|
120
|
+
//# 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.
|
|
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.2.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
|
}
|
|
@@ -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.
|
|
@@ -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 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. 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
|
+
}
|
package/templates/repo/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# repo/
|
|
2
2
|
|
|
3
3
|
The files `init` writes at the adopter's repository root, stored dot-less per the naming rule in the parent README: the git attributes and ignore templates, and the MCP server wiring for browser automation. `gitignore.qa` is a **fragment** of `gitignore` rather than a fourth file — the ignore rules for what the browser half of a run leaves in the working copy, rendered into the parent template only under the same `browserWiringApplies` gate as the MCP wiring, on the pattern `claude/settings.autonomous.qa.json` sets. It carries its own comments because they are gated with it: a repository that drives no browser must not carry prose explaining rules it does not have. Roadmap item 13 owns the writer. The MCP wiring template is written **only when the QA phase is enabled and its `qa.driver` is `web-playwright`**, so an adopter who never runs browser QA pays neither the browser tool schemas in every session's context nor a launched browser process — which is why it is a conditional write rather than part of the unconditional root set. A **mobile** driver gets a note and no file, for a second reason: the two mobile variants of the interactive test agent ship declared-not-implemented with built-ins-only tool allowlists, so a declared server would be wiring nothing can ever start. The permission profile's interactive-test fragment is gated on the same condition and in the same run — the profile names the servers it starts and this file declares them, so gating one side alone leaves the two disagreeing.
|
|
4
|
+
|
|
5
|
+
Two more templates carry the **docs-retrieval** half, gated on `retrievalApplies` (`phases.docs` and `docs.retrieval` both on) rather than on the QA phase. `mcp.retrieval.json` declares the `harness-docs` server, launched by `bash` on the repository's own `docs-search-server.sh`; `init` merges it with the browser template into **one** `.mcp.json`, written when either half applies and declaring only the halves that do. `gitignore.retrieval` is a fragment of `gitignore` on the `gitignore.qa` pattern, rendered into its `docsRetrievalIndex` token: the rule for the per-checkout index under the state directory, and its comment. The permission profile's docs-retrieval fragment, `claude/settings.autonomous.retrieval.json`, is gated on the same predicate for the reason given above.
|
package/templates/repo/gitignore
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment_docs_retrieval": "Docs-retrieval wiring, written by `autonomous-sdlc-harness init` only when `docs.retrieval` is on, and merged rather than overwritten on a re-run. The program is this repository's own docs-search-server.sh, in its configured scripts directory, which finds the machine-shared retrieval runtime `init` installed, so nothing is fetched at run time and no machine path is committed here. The generated permission profile starts this server and allows its one tool; turning retrieval on after that profile was written needs `init --force` to reach it.",
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"harness-docs": {
|
|
5
|
+
"type": "stdio",
|
|
6
|
+
"command": "bash",
|
|
7
|
+
"args": ["{{docsSearchServerPath}}"],
|
|
8
|
+
"env": {}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
The project-command wrapper scripts `init` writes into the configured `scriptsDir`: type-check, test and dev-server, configured through `commands.*` in `harness.config.json`, and deploy, configured through `deploy.command` on the separate `deploy` object — so no build tool and no hosting provider is hardcoded anywhere in the harness. They exist as scripts rather than as raw command lines because an unattended run's permission profile can allow-list a literal script path far more safely than an arbitrary command, which is why `commands.<key>` holds the **wrapper invocation** `bash <scriptsDir>/<name>.sh` — the literal the profile allow-lists — while the **raw** command line lives inside the script. Which raw line that is follows one precedence, and it is what keeps the pair from being circular: a raw command line already sitting in `commands.<key>` wins, because an adopter who edited it there meant it — and that state is **reported** rather than silently blessed, since the raw line is not the value the permission profile allow-lists: `init` names it as it writes the wrapper from it, `config set` names it as the value is stored, and `doctor`'s `command-wrappers` check grades it on every later run; otherwise, when that key holds this wrapper's own invocation — the normal state after a first `init` — the body is the line stack detection produced; and when neither resolves, the body names what to fix — the key to set, or for `deploy`, which detection never supplies, this script itself — and exits non-zero rather than appearing to have run a check it never ran. Two states get no wrapper at all, and therefore no allow entry: a key still holding the placeholder `init` writes for a command it could not detect, and `commands.typecheck` holding the `<none>` sentinel — the first is unfinished and is the one that asks the adopter to do something about it, the second is the answer that this repository has no such command and asks for nothing. **Each script prints its own verdict line**, so a caller never appends an exit-code probe to it: that compound form is exactly what stalls an unattended run on a permission prompt — except `start-dev-server.sh`, which has no verdict to give because it starts a long-running process. Every wrapper anchors itself to its own checkout before it runs and forwards whatever arguments it was given to the **last** command of its raw line — whether that command accepts a bare path or name filter is a property of the command, not of the wrapper, so a line ending in a sub-command's own flags takes none; `docs/cli.md` §5 is the full contract.
|
|
4
4
|
|
|
5
|
-
**The other family in this directory is not generated.** The run watcher, its restart wrapper, the notifier and its stream formatter, the commit / push / branch-refresh / worktree / cleanup wrappers, the scratch runner and the shared library at `lib/harness-run-lib.sh` are copied byte for byte into the same `scriptsDir`, because each reads `harness.config.json` *at run time* rather than carrying a value frozen in when `init` ran — a guard whose protected-branch set was baked into a file at generation time enforces the wrong set the moment that list changes, and does it silently. They therefore carry no `{{token}}` at all. `cli/src/generators/outerLoopScripts.ts` declares that set and marks which of its rows an agent may invoke; `cli/scripts/README.md` records why they live under `scriptsDir` rather than in the installed package. **One of them runs a file it is given:** `scratch-run.sh` runs an agent's language probe or mutation check in the interpreter that file's extension names, and refuses any argument that does not resolve inside `<state_dir>/scratch/`.
|
|
5
|
+
**The other family in this directory is not generated.** The run watcher, its restart wrapper, the notifier and its stream formatter, the commit / push / branch-refresh / worktree / cleanup wrappers, the scratch runner, the docs-retrieval server launcher and the shared library at `lib/harness-run-lib.sh` are copied byte for byte into the same `scriptsDir`, because each reads `harness.config.json` *at run time* rather than carrying a value frozen in when `init` ran — a guard whose protected-branch set was baked into a file at generation time enforces the wrong set the moment that list changes, and does it silently. They therefore carry no `{{token}}` at all. `cli/src/generators/outerLoopScripts.ts` declares that set and marks which of its rows an agent may invoke; `cli/scripts/README.md` records why they live under `scriptsDir` rather than in the installed package. **One of them runs a file it is given:** `scratch-run.sh` runs an agent's language probe or mutation check in the interpreter that file's extension names, and refuses any argument that does not resolve inside `<state_dir>/scratch/`. **One of them is started by neither the watcher nor an agent:** `docs-search-server.sh` is started by the agent runner from `.mcp.json` when `docs.retrieval` is on, and `exec`s the machine-shared retrieval runtime's `docs serve`.
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
# completed branch, a parked question or a failed run is noticed at all.
|
|
11
11
|
#
|
|
12
12
|
# THE EVENT VOCABULARY IS A CONTRACT. The watcher's exit classifier and its
|
|
13
|
-
# launch, resume, park and stall paths all call this script with one of these
|
|
14
|
-
# words, and nothing else may be added here without adding it there:
|
|
13
|
+
# launch, resume, park and stall paths all call this script with one of these
|
|
14
|
+
# seven words, and nothing else may be added here without adding it there:
|
|
15
15
|
#
|
|
16
16
|
# completed the run reached "branch ready for review"
|
|
17
17
|
# parked the run wrote a clarification question and yielded — idle until
|
|
18
18
|
# the answer file lands
|
|
19
|
+
# park_loop the watcher stopped resuming a parked run whose resumes made no
|
|
20
|
+
# progress — idle until an operator clears it
|
|
19
21
|
# paused the run honored a PAUSE request and yielded — idle until RESUME
|
|
20
22
|
# failed the run exited non-zero, or its process vanished
|
|
21
23
|
# launched a fresh inbox run was just started
|
|
@@ -98,7 +100,7 @@
|
|
|
98
100
|
#
|
|
99
101
|
# Usage:
|
|
100
102
|
# autonomous-notify.sh <event> <branch> [log_path] [detail]
|
|
101
|
-
# event completed | parked | paused | failed | launched | resumed
|
|
103
|
+
# event completed | parked | park_loop | paused | failed | launched | resumed
|
|
102
104
|
# branch the run's branch name
|
|
103
105
|
# log_path optional path to the central run log, shown in the message
|
|
104
106
|
# detail optional one-line extra, e.g. the clarification question's file
|
|
@@ -140,7 +142,7 @@ LOG_PATH="${3:-}"
|
|
|
140
142
|
DETAIL="${4:-}"
|
|
141
143
|
|
|
142
144
|
if [ -z "$EVENT" ] || [ -z "$BRANCH" ]; then
|
|
143
|
-
echo "usage: $self <completed|parked|paused|failed|launched|resumed> <branch> [log_path] [detail]" >&2
|
|
145
|
+
echo "usage: $self <completed|parked|park_loop|paused|failed|launched|resumed> <branch> [log_path] [detail]" >&2
|
|
144
146
|
exit 2
|
|
145
147
|
fi
|
|
146
148
|
|
|
@@ -197,6 +199,10 @@ parked)
|
|
|
197
199
|
TITLE="[$slug] Run parked — $BRANCH"
|
|
198
200
|
MESSAGE="Run '$BRANCH' is waiting for a clarification answer."
|
|
199
201
|
;;
|
|
202
|
+
park_loop)
|
|
203
|
+
TITLE="[$slug] Run stuck in a park loop — $BRANCH"
|
|
204
|
+
MESSAGE="Run '$BRANCH' parked again after resumes that made no progress; the watcher has stopped resuming it."
|
|
205
|
+
;;
|
|
200
206
|
paused)
|
|
201
207
|
TITLE="[$slug] Run paused — $BRANCH"
|
|
202
208
|
MESSAGE="Run '$BRANCH' honored a PAUSE request and yielded. $resume_hint"
|