@khoavu882/doflow 2.0.0 → 2.0.2
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 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +21 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +26 -2
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +176 -503
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +12 -16
- package/core/registry/storage.json +9 -2
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
- package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +23 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +11 -5
- package/core/shared/skills/do-design/references/data_model_example.md +167 -0
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/templates/doflow/data-model-template.md +136 -102
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +54 -12
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/agents.js +15 -1
- package/src/adapters/codex/config.js +5 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copilot/index.js +11 -4
- package/src/adapters/copy-tree.js +8 -2
- package/src/adapters/created-file.js +41 -0
- package/src/adapters/mcp-entries.js +11 -1
- package/src/adapters/opencode/index.js +32 -5
- package/src/adapters/pi/index.js +15 -4
- package/src/adapters/pi/mcp.js +44 -18
- package/src/cli/commands/remove.js +3 -1
- package/src/cli/commands/status.js +1 -1
- package/src/cli/commands/update.js +7 -3
- package/src/cli/index.js +43 -19
- package/src/cli/runtime-commands.js +13 -5
- package/src/cli/shared.js +5 -3
- package/src/helper/marker-merge.js +11 -7
- package/src/install/backup-set.js +7 -3
- package/src/install/backup.js +13 -7
- package/src/install/context.js +2 -1
- package/src/install/manifest.js +35 -11
- package/src/install/paths.js +38 -0
- package/src/install/targets.js +11 -2
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/lifecycle/index.js +80 -3
- package/src/lifecycle/retired-harnesses.js +16 -1
- package/src/lifecycle/view.js +3 -2
- package/src/registry/index.js +21 -4
- package/src/release/docs-blocks.js +37 -53
- package/src/runtime/advice/doctor.js +2 -2
- package/src/runtime/brief-tools.js +2 -1
- package/src/runtime/capability-router.js +26 -3
- package/src/runtime/context/pointer.js +84 -17
- package/src/runtime/context-pack.js +52 -9
- package/src/runtime/dispatch/integrate.js +27 -8
- package/src/runtime/dispatch/loop.js +7 -0
- package/src/runtime/dispatch/verify-run.js +9 -3
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/feature-pointer.js +10 -1
- package/src/runtime/health.js +68 -5
- package/src/runtime/implementation-gate.js +14 -6
- package/src/runtime/intake/engine.js +679 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/knowledge/code-retrieval.js +11 -5
- package/src/runtime/knowledge/semble-provider.js +12 -2
- package/src/runtime/lifecycle/cli.js +76 -6
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +34 -3
- package/src/runtime/lifecycle/release.js +16 -3
- package/src/runtime/lifecycle/retention.js +12 -15
- package/src/runtime/lifecycle/store-upkeep.js +13 -3
- package/src/runtime/mask.js +3 -3
- package/src/runtime/mcp-consultations.js +11 -5
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/research-request.js +3 -1
- package/src/runtime/review/command.js +3 -7
- package/src/runtime/review/start.js +24 -10
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +127 -33
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +15 -5
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/files.js +3 -1
- package/src/runtime/settings/prepared-run.js +12 -2
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/settings/resolve.js +13 -1
- package/src/runtime/settings/write.js +73 -2
- package/src/runtime/slots.js +1 -1
- package/src/runtime/storage/registry.js +2 -2
- package/src/runtime/storage/roots.js +4 -18
- package/src/runtime/storage/rules.js +22 -46
- package/src/runtime/storage/work-status.js +14 -4
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-scope.js +17 -2
- package/src/runtime/task-state.js +13 -7
- package/src/runtime/trace/analyze.js +4 -2
- package/src/runtime/trace/indicators.js +25 -20
- package/src/runtime/trace/spans.js +4 -5
- package/src/runtime/verb-view.js +268 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/command.js +4 -2
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +10 -6
- package/src/runtime/worker/supervisor.js +18 -6
- package/src/runtime/workflow-orchestrator.js +39 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
|
|
5
|
-
const POLICY_ORDER = Object.freeze(['pre-implementation-gate', 'pre-bash-guard', 'session-context', 'mcp-tool-guard', 'stop-check']);
|
|
6
5
|
const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
|
|
7
6
|
const BEHAVIOUR_ORDER = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
|
|
8
7
|
const NEVER_BLOCKING_EVENTS = new Set([
|
|
@@ -43,32 +42,4 @@ function enforcementFor(coverage , query
|
|
|
43
42
|
reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
|
|
44
43
|
};
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
const order = (items , value ) => {
|
|
48
|
-
const index = items.indexOf(value);
|
|
49
|
-
return index < 0 ? items.length : index;
|
|
50
|
-
};
|
|
51
|
-
const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
|
|
52
|
-
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
53
|
-
|| a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
|
|
54
|
-
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
|
|
55
|
-
for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
|
|
56
|
-
return lines.join('\n');
|
|
57
|
-
}
|
|
58
|
-
// IC-019: one row per behaviour x harness, ordered by IC-018's behaviour list, then HARNESS_ORDER.
|
|
59
|
-
function renderBehaviourMatrix(coverage ) {
|
|
60
|
-
const order = (items , value ) => {
|
|
61
|
-
const index = items.indexOf(value);
|
|
62
|
-
return index < 0 ? items.length : index;
|
|
63
|
-
};
|
|
64
|
-
const entries = [...(coverage.behaviours || [])].sort((a , b ) => order(BEHAVIOUR_ORDER, a.behaviour) - order(BEHAVIOUR_ORDER, b.behaviour)
|
|
65
|
-
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
66
|
-
|| a.behaviour.localeCompare(b.behaviour) || a.harness.localeCompare(b.harness));
|
|
67
|
-
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}`, '| Behaviour | Harness | Status | Limitation or tests |', '|---|---|---|---|'];
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const detail = entry.status === 'enforced' ? (entry.tests || []).join(', ') : (entry.limitation || '');
|
|
70
|
-
lines.push(`| ${entry.behaviour} | ${entry.harness} | ${entry.status} | ${detail} |`);
|
|
71
|
-
}
|
|
72
|
-
return lines.join('\n');
|
|
73
|
-
}
|
|
74
|
-
module.exports = { enforcementFor, renderEnforcementMatrix, renderBehaviourMatrix, BEHAVIOUR_ORDER, HARNESS_ORDER, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
45
|
+
module.exports = { enforcementFor, BEHAVIOUR_ORDER, HARNESS_ORDER, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
@@ -39,13 +39,13 @@ function resolveHookTier({ env = process.env, loadSettings }
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** One WARN line per planned harness that gets no hooks although it is the primary session (claude)
|
|
42
|
-
* or a worker the person configured: there it runs without DoFlow's
|
|
42
|
+
* or a worker the person configured: there it runs without DoFlow's gates. */
|
|
43
43
|
function hookTierWarnings(registry , targets , tier ) {
|
|
44
44
|
const warnings = [];
|
|
45
45
|
for (const harness of registry.harnesses) {
|
|
46
46
|
if (!targets.includes(harness.id) || tier.harnesses.includes(harness.id) || !getsHooks(registry, harness)) continue;
|
|
47
47
|
const role = harness.id === 'claude' ? 'the primary session' : tier.workers?.includes(harness.id) ? `a worker in ${WORKERS_KEY}` : null;
|
|
48
|
-
if (role) warnings.push(`${harness.id} is ${role} but not in ${HOOKS_KEY}: it runs without DoFlow's hooks
|
|
48
|
+
if (role) warnings.push(`${harness.id} is ${role} but not in ${HOOKS_KEY}: it runs without DoFlow's hooks or gates`);
|
|
49
49
|
}
|
|
50
50
|
return warnings;
|
|
51
51
|
}
|
package/src/lifecycle/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const { renderMcpIndex } = require('./mcp-index.js');
|
|
|
17
17
|
const { hasBashCapableShell } = require('./bash-availability.js');
|
|
18
18
|
const { planHookCompanions } = require('./hook-step.js');
|
|
19
19
|
const { tieredRegistry, hookHoldersFromLedger, hooksTieredOff, withoutHookSources, planHookTierRemovals } = require('./hook-tier.js');
|
|
20
|
+
const { forgetManifestTools } = require('../install/manifest.js');
|
|
20
21
|
const { readLock, lockPackages } = require('../state/lockfile.js');
|
|
21
22
|
const { resolveRegistrySource } = require('../adapters/resolve-source.js');
|
|
22
23
|
const { planRetiredHarnesses, planHandovers, handoversFor, applyRetiredHarnesses, retiredReleaseKeys, claimKey } = require('./retired-harnesses.js');
|
|
@@ -118,6 +119,34 @@ function markUnownedAbsences(statuses , ledger , harness ) {
|
|
|
118
119
|
));
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/** An adapter verifies every asset the current source declares for it, comparing disk to source. For
|
|
123
|
+
* an asset the removal leaves this harness nothing to delete from, that is not its business: the
|
|
124
|
+
* files belong to another harness's rows (retained removals, or an older install that gave this
|
|
125
|
+
* harness no rows there: a v1.14.2 Pi install recorded none for the guidance, scripts and runtime
|
|
126
|
+
* trees) and so carry that install's legacy bytes. Those bytes read as conflicts on files this
|
|
127
|
+
* removal never touches, and the ones that do match source would be written back as NEW rows for
|
|
128
|
+
* the harness being removed. So for such an asset the `conflict` status, its `<assetId>: ...`
|
|
129
|
+
* conflict strings and its copy-tree resources are dropped. An asset the plan deletes from, or one
|
|
130
|
+
* where a ledger row has no removal, is judged exactly as before. Without a ledger nothing is
|
|
131
|
+
* proven settled, so the strict reading is kept. */
|
|
132
|
+
function dropSettledAssetVerification(verification , statuses , conflicts , removals , ledger ) {
|
|
133
|
+
const unchanged = { resources: verification.resources, statuses, conflicts };
|
|
134
|
+
if (!ledger) return unchanged;
|
|
135
|
+
const rows = (ledger.resources || []).filter((resource ) => resource.harness === verification.harness);
|
|
136
|
+
const settled = (assetId ) => !removals.some((change ) => change.assetId === assetId && !change.retained)
|
|
137
|
+
&& !rows.some((row ) => row.assetId === assetId && !removals.some((change ) => matchesChange(row, change)));
|
|
138
|
+
const settledAssets = new Set([
|
|
139
|
+
...statuses.filter((status ) => status?.status === 'conflict').map((status ) => status.assetId),
|
|
140
|
+
...verification.resources.filter((resource ) => resource?.kind === 'copy-tree-file').map((resource ) => resource.assetId),
|
|
141
|
+
].filter((assetId ) => assetId && settled(assetId)));
|
|
142
|
+
if (!settledAssets.size) return unchanged;
|
|
143
|
+
return {
|
|
144
|
+
resources: verification.resources.filter((resource ) => !(resource?.kind === 'copy-tree-file' && settledAssets.has(resource.assetId))),
|
|
145
|
+
statuses: statuses.map((status ) => (settledAssets.has(status?.assetId) && status.status === 'conflict' ? { ...status, status: 'not-managed' } : status)),
|
|
146
|
+
conflicts: conflicts.filter((reason ) => ![...settledAssets].some((assetId) => String(reason).startsWith(`${assetId}: `))),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
function normalizeRemovalVerification(verification , changes , ledger ) {
|
|
122
151
|
const removals = changes.filter((change ) => change.operation === 'remove' && change.harness === verification.harness);
|
|
123
152
|
if (!removals.length) return verification;
|
|
@@ -128,7 +157,9 @@ function normalizeRemovalVerification(verification , changes , ledger
|
|
|
128
157
|
// before.
|
|
129
158
|
const removed = removals.filter((change ) => !change.retained);
|
|
130
159
|
let statuses = [...verification.statuses];
|
|
131
|
-
|
|
160
|
+
let conflicts = [...(verification.conflicts || [])];
|
|
161
|
+
let resources ;
|
|
162
|
+
({ resources, statuses, conflicts } = dropSettledAssetVerification(verification, statuses, conflicts, removals, ledger));
|
|
132
163
|
for (const change of removed) {
|
|
133
164
|
const outcome = reconcileRemovedChange(verification, statuses, change);
|
|
134
165
|
if (outcome.conflict) { conflicts.push(outcome.conflict); continue; }
|
|
@@ -137,7 +168,7 @@ function normalizeRemovalVerification(verification , changes , ledger
|
|
|
137
168
|
}
|
|
138
169
|
// After reconciling the plan's own removals, so this only ever judges what the plan left over.
|
|
139
170
|
statuses = markUnownedAbsences(statuses, ledger, verification.harness);
|
|
140
|
-
return { ...verification, statuses, conflicts, ok: removalVerificationOk(verification.ok, statuses, conflicts) };
|
|
171
|
+
return { ...verification, resources, statuses, conflicts, ok: removalVerificationOk(verification.ok, statuses, conflicts) };
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
/** An adapter may initially report ok:false because a now-removed resource is missing. Once
|
|
@@ -913,7 +944,15 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
913
944
|
// Record the relocation, then sweep the stale copy — but only when its bytes still hash to the
|
|
914
945
|
// fingerprint DoFlow last verified there. A mismatch means someone edited that file after us:
|
|
915
946
|
// their content outranks our tidiness, so the file stays and the unswept tombstone says why.
|
|
947
|
+
// An MCP entry is one member of a file the user shares, and the adapter has already taken it out
|
|
948
|
+
// of (or released it in) the old file by entry. A whole-file tombstone for it would carry the
|
|
949
|
+
// entry's fingerprint, never match the file's bytes and so stay unswept for good. Earlier runs
|
|
950
|
+
// left such rows behind for Pi's mcp.json (which borrows the guidance.codex-pointer asset id);
|
|
951
|
+
// they are retired here, and no file is touched.
|
|
952
|
+
next.tombstones = next.tombstones.filter((entry ) => entry.sweptAt || entry.harness !== 'pi'
|
|
953
|
+
|| entry.assetId !== 'guidance.codex-pointer' || path.basename(String(entry.fromTarget)) !== 'mcp.json');
|
|
916
954
|
for (const resource of next.resources) {
|
|
955
|
+
if (resource.kind === 'mcp-server') continue;
|
|
917
956
|
const prior = priorByIdentity.get(`${resource.harness}\u0000${resource.assetId}\u0000${resource.identity ?? ''}`);
|
|
918
957
|
if (!prior || prior.target === resource.target) continue;
|
|
919
958
|
if (!next.tombstones.some((entry ) => entry.harness === resource.harness
|
|
@@ -925,6 +964,16 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
925
964
|
});
|
|
926
965
|
}
|
|
927
966
|
}
|
|
967
|
+
// A legacy retirement: an adapter removed an old projection that no row owned (its change carries
|
|
968
|
+
// `legacyRetirement.toTarget`, where the claim lives now). Journaled as a tombstone already swept.
|
|
969
|
+
for (const change of changes) {
|
|
970
|
+
if (!change.legacyRetirement || change.operation !== 'remove' || change.retained) continue;
|
|
971
|
+
const { harness, assetId, target: fromTarget, fingerprint } = change;
|
|
972
|
+
const toTarget = change.legacyRetirement.toTarget;
|
|
973
|
+
if (next.tombstones.some((entry ) => entry.harness === harness && entry.fromTarget === fromTarget && entry.toTarget === toTarget)) continue;
|
|
974
|
+
const at = new Date().toISOString();
|
|
975
|
+
next.tombstones.push({ harness, assetId, fromTarget, toTarget, fingerprint, movedAt: at, sweptAt: at });
|
|
976
|
+
}
|
|
928
977
|
const claims = new Set(next.resources.map((resource ) => resource.target));
|
|
929
978
|
for (const entry of next.tombstones) {
|
|
930
979
|
if (entry.sweptAt || !fsImpl.existsSync(entry.fromTarget) || claims.has(entry.fromTarget)) continue;
|
|
@@ -1152,9 +1201,37 @@ function settleSuspendedProjection({ homeDir } ) {
|
|
|
1152
1201
|
}
|
|
1153
1202
|
}
|
|
1154
1203
|
|
|
1204
|
+
/** Remove the directories a removal emptied, from each removed file's folder upward, tool root
|
|
1205
|
+
* included (`.pi`, `.opencode`, `.agents`); `rmdir` refuses a folder that holds anything, so no
|
|
1206
|
+
* file is touched. The walk stops at the scope root and never takes a shared `.config` folder. */
|
|
1207
|
+
function pruneEmptiedDirs(changes , scopeRoot , fsImpl = fs) {
|
|
1208
|
+
const root = path.resolve(scopeRoot);
|
|
1209
|
+
for (const change of changes) {
|
|
1210
|
+
if (change.operation !== 'remove' || change.retained || change.release || typeof change.target !== 'string') continue;
|
|
1211
|
+
let dir = path.dirname(path.resolve(change.target));
|
|
1212
|
+
if (!isUnder(dir, root)) continue;
|
|
1213
|
+
// The removed file's own folders may already be gone; the first one still standing starts the walk.
|
|
1214
|
+
while (dir !== root && isUnder(dir, root) && !fsImpl.existsSync(dir)) dir = path.dirname(dir);
|
|
1215
|
+
while (dir !== root && isUnder(dir, root) && path.basename(dir) !== '.config') {
|
|
1216
|
+
try { fsImpl.rmdirSync(dir); } catch { break; }
|
|
1217
|
+
dir = path.dirname(dir);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Stop the install manifest listing a harness the removal left with no ownership row. */
|
|
1223
|
+
function forgetRemovedTools(targets , ledger , scopeRoot ) {
|
|
1224
|
+
const standing = new Set((ledger?.resources || []).map((resource ) => resource.harness));
|
|
1225
|
+
const gone = (targets || []).filter((harness) => !standing.has(harness));
|
|
1226
|
+
if (gone.length) forgetManifestTools({ scopeRoot, tools: gone });
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1155
1229
|
function removeLifecycle(options ) {
|
|
1156
1230
|
const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
|
|
1157
|
-
|
|
1231
|
+
const result = applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
|
|
1232
|
+
pruneEmptiedDirs(plan.targets.flatMap((target ) => target.changes), plan.scopeRoot);
|
|
1233
|
+
forgetRemovedTools(options.targets, result.ledger, plan.scopeRoot);
|
|
1234
|
+
return result;
|
|
1158
1235
|
}
|
|
1159
1236
|
|
|
1160
1237
|
module.exports = { OPERATIONS, MCP_KEEP, recordMcpOwnership, isPackageAsset, registryScope, normalizeTargets, normalizeChange, matchesChange, normalizeRemovalVerification, adapterConflicts, planLifecycle, verifyLifecycle, applyLifecycle, removeLifecycle, updateLedger, mcpIndexPath, applyMcpIndex, targetNeedsHooks, assertBashAvailableForHooks, hookWiringStatus, markRetainedRemovals, retentionSummary, PRUNED_TREES, settleSuspendedProjection };
|
|
@@ -128,6 +128,20 @@ function markerState(text ) { return markedS
|
|
|
128
128
|
/** Rule 3a's verdict for one retired row (IC-003, IC-007, design D7). */
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
/** The one rule for "the replacement manages this file": at project scope the replacement writes its
|
|
132
|
+
* declared instruction file whatever else shares it; at global scope it stays out while another live
|
|
133
|
+
* harness in `registry` declares the same file (that harness's adapter keeps it). The handover plan
|
|
134
|
+
* here and antigravity's `instructionTarget` both ask it, so a row is never released to a harness that
|
|
135
|
+
* then plans nothing. */
|
|
136
|
+
function replacementManagesFile({ registry, harnessId, surface, target, scope, scopeRoot } ) {
|
|
137
|
+
if (scope !== 'global') return true;
|
|
138
|
+
const declaredBy = (harness ) => {
|
|
139
|
+
if (harness?.id === harnessId || !harness?.paths?.[surface]) return false;
|
|
140
|
+
try { return resolveHarnessPaths({ [surface]: harness.paths[surface] }, { scope: 'global', scopeRoot, homeDir: scopeRoot })[surface] === target; } catch { return false; }
|
|
141
|
+
};
|
|
142
|
+
return !(registry?.harnesses ?? []).some(declaredBy);
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
/**
|
|
132
146
|
* Rule 3a, decided before the live harnesses plan, because the replacement needs the verdict to plan
|
|
133
147
|
* its own change: `context.handovers` for each `to`. A retired row qualifies when a `removal.handover`
|
|
@@ -155,6 +169,7 @@ function planHandovers({ registry, ledger, scope, scopeRoot, planned = [], fsImp
|
|
|
155
169
|
let declared = null;
|
|
156
170
|
try { declared = resolveHarnessPaths({ [entry.surface]: to.paths[entry.surface] }, { scope, scopeRoot, homeDir: scopeRoot })[entry.surface]; } catch { continue; }
|
|
157
171
|
if (declared !== row.target || !fsImpl.existsSync(row.target) || !resolvesInPlace(row.target, scopeRoot, fsImpl)) continue;
|
|
172
|
+
if (!replacementManagesFile({ registry, harnessId: to.id, surface: entry.surface, target: declared, scope, scopeRoot })) continue;
|
|
158
173
|
const base = { to: to.id, target: row.target, fromKey: ownershipKey(row) };
|
|
159
174
|
if (plannedIds.has(to.id)) {
|
|
160
175
|
decisions.set(base.fromKey, { action: 'handover', ...base, edited: sha256(fsImpl.readFileSync(row.target)) !== row.fingerprint });
|
|
@@ -315,4 +330,4 @@ function formatRetiredPlans(plans ) {
|
|
|
315
330
|
]);
|
|
316
331
|
}
|
|
317
332
|
|
|
318
|
-
module.exports = { planRetiredHarnesses, planHandovers, handoversFor, applyRetiredHarnesses, retiredReleaseKeys, formatRetiredPlans, claimKey };
|
|
333
|
+
module.exports = { replacementManagesFile, planRetiredHarnesses, planHandovers, handoversFor, applyRetiredHarnesses, retiredReleaseKeys, formatRetiredPlans, claimKey };
|
package/src/lifecycle/view.js
CHANGED
|
@@ -19,6 +19,7 @@ const { createCopilotAdapter } = require('../adapters/copilot/index.js');
|
|
|
19
19
|
const { createKiroAdapter } = require('../adapters/kiro/index.js');
|
|
20
20
|
const { createAntigravityAdapter } = require('../adapters/antigravity/index.js');
|
|
21
21
|
const { planLifecycle } = require('./index.js');
|
|
22
|
+
const { canonicalPath, canonicalLedger } = require('../install/paths.js');
|
|
22
23
|
const { formatRetiredPlans } = require('./retired-harnesses.js');
|
|
23
24
|
const { resolveHookTier, hookTierWarnings } = require('./hook-tier.js');
|
|
24
25
|
const { stateRoot, readLedger, defaultLedger } = require('../state/index.js');
|
|
@@ -51,9 +52,9 @@ function codexConfigResources(repoRoot , fsImpl ) {
|
|
|
51
52
|
* reloaded — the same registry also resolves every harness's MCP selection for that command. */
|
|
52
53
|
function registryLifecycleView({ registry, scope, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], operation, repoRoot, force = false, adopt = false, permissions = false, statusline = false, hookHarnesses, fsImpl = fs } ) {
|
|
53
54
|
const lifecycleScope = codexScope(scope);
|
|
54
|
-
const scopeRoot = scope.global ? os.homedir() :
|
|
55
|
+
const scopeRoot = scope.global ? os.homedir() : canonicalPath(scope.projectRoot);
|
|
55
56
|
const neutralStateRoot = stateRoot({ scope: lifecycleScope, projectRoot: scopeRoot, homeDir: scopeRoot });
|
|
56
|
-
const ledger = readLedger(neutralStateRoot) ?? defaultLedger({ scope: lifecycleScope, scopeRoot });
|
|
57
|
+
const ledger = canonicalLedger(readLedger(neutralStateRoot), scopeRoot) ?? defaultLedger({ scope: lifecycleScope, scopeRoot });
|
|
57
58
|
const tier = hookHarnesses ? { harnesses: hookHarnesses, workers: null } : resolveHookTier();
|
|
58
59
|
// Mirrors buildAdapterRegistry() in src/cli/shared.js — including the declared-paths wiring —
|
|
59
60
|
// and is kept as a literal construction here because the registry guard pins every declared
|
package/src/registry/index.js
CHANGED
|
@@ -582,6 +582,8 @@ const WORKER_BILLING = new Set(['free', 'metered', 'unknown']);
|
|
|
582
582
|
const WORKER_KEYS = Object.freeze(['profile', 'billing', ...Object.keys(WORKER_CAPABILITIES), 'modelChecks']);
|
|
583
583
|
const WORKER_ALLOWED_KEYS = Object.freeze([...WORKER_KEYS, ...Object.keys(WORKER_OPTIONAL_CAPABILITIES), 'family']);
|
|
584
584
|
const WORKER_ENTRY_FIELDS = new Set(['status', 'evidence', 'verification', 'verifiedOn', 'reason']);
|
|
585
|
+
// Soft hyphen, Mongolian vowel separator, zero-width and directional marks, word joiner and invisible operators, BOM.
|
|
586
|
+
const INVISIBLE_CHARACTER = /[\u00ad\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/;
|
|
585
587
|
const WORKER_RECIPE_FIELDS = new Set(['binary', 'versionArgv', 'argv', 'promptVia', 'parser', 'marker', 'quotaSignals']);
|
|
586
588
|
const WORKER_QUOTA_SIGNALS_FIELDS = new Set(['signals', 'basis', 'evidence']);
|
|
587
589
|
const WORKER_FACTS = new Set(['hooksLoaded', 'identityForwarded', 'guidanceLoaded', 'login']);
|
|
@@ -681,10 +683,25 @@ function validateWorker(value , location , errors ) {
|
|
|
681
683
|
else {
|
|
682
684
|
for (const field of Object.keys(quota)) if (!WORKER_QUOTA_SIGNALS_FIELDS.has(field)) fail(key, `quotaSignals has unknown field '${field}'`);
|
|
683
685
|
if (!Array.isArray(quota.signals) || quota.signals.length < 1 || quota.signals.length > 16) fail(key, 'quotaSignals.signals must contain 1 to 16 signals');
|
|
684
|
-
else
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
686
|
+
else {
|
|
687
|
+
const folded = new Set ();
|
|
688
|
+
for (const signal of quota.signals) {
|
|
689
|
+
if (typeof signal !== 'string' || signal !== signal.trim() || signal.length < 3 || signal.length > 64 || /[\u0000-\u001f\u007f]/.test(signal)) {
|
|
690
|
+
fail(key, 'quotaSignals.signals must be trimmed strings of 3 to 64 characters without control characters');
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
// The matcher lowercases both sides, so a signal that differs only by case never adds a match; a
|
|
694
|
+
// zero-width or format character makes one that reads like another yet matches nothing.
|
|
695
|
+
if (INVISIBLE_CHARACTER.test(signal)) {
|
|
696
|
+
fail(key, 'quotaSignals.signals must not contain zero-width or invisible format characters');
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
const lowered = signal.toLowerCase();
|
|
700
|
+
if (folded.has(lowered)) {
|
|
701
|
+
fail(key, `quotaSignals.signals has a duplicate signal '${signal}' (compared without letter case)`);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
folded.add(lowered);
|
|
688
705
|
}
|
|
689
706
|
}
|
|
690
707
|
if (quota.basis !== 'unverified' && quota.basis !== 'transcript') fail(key, "quotaSignals.basis must be 'unverified' or 'transcript'");
|
|
@@ -20,7 +20,6 @@ const os = require('node:os');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const { spawnSync } = require('node:child_process');
|
|
22
22
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
23
|
-
const { renderEnforcementMatrix, renderBehaviourMatrix } = require('../lifecycle/coverage.js');
|
|
24
23
|
const { loadSettingsRegistry } = require('../runtime/settings/registry.js');
|
|
25
24
|
const { REF_META } = require('../runtime/settings/refs.js');
|
|
26
25
|
|
|
@@ -36,13 +35,6 @@ const DOFLOW_RUN_BIN = 'core/shared/scripts/doflow/bin/doflow-run';
|
|
|
36
35
|
|
|
37
36
|
const NUMBER_WORDS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
|
|
38
37
|
|
|
39
|
-
// Display labels for capability keys; a key without a label is shown capitalized, so a capability
|
|
40
|
-
// added to the registry still gets its row.
|
|
41
|
-
const CAPABILITY_LABELS = {
|
|
42
|
-
instructions: 'Instructions', skills: 'Skills', agents: 'Agents', scripts: 'Scripts', templates: 'Templates',
|
|
43
|
-
modes: 'Modes', settings: 'Settings', hooks: 'Hooks', mcp: 'MCP', plugin: 'Plugin / extension',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
38
|
|
|
47
39
|
|
|
48
40
|
|
|
@@ -94,38 +86,8 @@ function table(headers , rows ) {
|
|
|
94
86
|
].join('\n');
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
/** A copy of registry data with every string made inert, for renderers that format cells themselves. */
|
|
98
|
-
function inert(value ) {
|
|
99
|
-
if (typeof value === 'string') return value === '' ? value : text(value);
|
|
100
|
-
if (Array.isArray(value)) return value.map(inert);
|
|
101
|
-
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, inert(entry)]));
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function capitalize(text ) {
|
|
106
|
-
return text ? text[0].toUpperCase() + text.slice(1) : text;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
89
|
// ── Renderers (one pure function of the root per block id) ──────────────────────────────────────
|
|
110
90
|
|
|
111
|
-
function renderCapabilityMatrix(root ) {
|
|
112
|
-
const harnesses = readJson(root, 'core/registry/harnesses.json').harnesses;
|
|
113
|
-
if (!Array.isArray(harnesses) || harnesses.length === 0) throw new Error('harnesses.json has no harnesses');
|
|
114
|
-
const keys = Object.keys(harnesses[0].capabilities || {});
|
|
115
|
-
if (keys.length === 0) throw new Error('harnesses.json declares no capabilities');
|
|
116
|
-
const status = (harness , key ) => {
|
|
117
|
-
const value = harness.capabilities?.[key]?.status;
|
|
118
|
-
if (typeof value !== 'string' || !value) return '—';
|
|
119
|
-
const target = harness.nativeTargets?.[key];
|
|
120
|
-
const label = text(capitalize(value));
|
|
121
|
-
return target && (value === 'supported' || value === 'different') ? `${label} — ${code(target)}` : label;
|
|
122
|
-
};
|
|
123
|
-
return table(
|
|
124
|
-
['Capability', ...harnesses.map((harness ) => oneLine(harness.displayName))],
|
|
125
|
-
keys.map((key) => [text(CAPABILITY_LABELS[key] || capitalize(key)), ...harnesses.map((harness ) => status(harness, key))]),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
91
|
function renderTaskClasses(root ) {
|
|
130
92
|
const classes = readJson(root, 'core/registry/workflows.json').classes;
|
|
131
93
|
const templates = new Set(Object.keys(readJson(root, 'core/registry/readiness-templates.json').templates || {}));
|
|
@@ -287,13 +249,31 @@ function renderCommandIndex(root ) {
|
|
|
287
249
|
].join('\n');
|
|
288
250
|
}
|
|
289
251
|
|
|
252
|
+
/** One rule as a short phrase: its extensions, a name filter, and where it files or what it follows. */
|
|
253
|
+
function ruleText(rule ) {
|
|
254
|
+
const parts = [rule.extensions.map((ext ) => `.${ext}`).join(' ')];
|
|
255
|
+
if (rule.namePattern) parts.push(`name matches ${rule.namePattern}`);
|
|
256
|
+
if (rule.companionOf) parts.push(`beside a ${rule.companionOf.map((ext ) => `.${ext}`).join(' ')} file`);
|
|
257
|
+
if (rule.into) parts.push(`into ${rule.into}/`);
|
|
258
|
+
return parts.join(', ');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderIntakeCategories(root ) {
|
|
262
|
+
const categories = readJson(root, 'core/registry/intake.json').categories;
|
|
263
|
+
if (!Array.isArray(categories) || categories.length === 0) throw new Error('intake.json has no categories');
|
|
264
|
+
return table(['Folder', 'Also accepted as', 'Takes', 'Purpose'], categories.map((category ) => [
|
|
265
|
+
code(category.id),
|
|
266
|
+
text(category.names.join(', ')),
|
|
267
|
+
text(category.rules.map(ruleText).join('; ')),
|
|
268
|
+
text(category.summary),
|
|
269
|
+
]));
|
|
270
|
+
}
|
|
271
|
+
|
|
290
272
|
const BLOCKS = Object.freeze([
|
|
291
|
-
{ id: 'capability-matrix', page: 'capability-map.md', sources: ['core/registry/harnesses.json'], render: renderCapabilityMatrix },
|
|
292
|
-
{ id: 'enforcement-matrix', page: 'capability-map.md', sources: ['core/registry/lifecycle.json'], render: (root ) => renderEnforcementMatrix(inert(readJson(root, 'core/registry/lifecycle.json').coverage)) },
|
|
293
|
-
{ id: 'context-behaviour-matrix', page: 'capability-map.md', sources: ['core/registry/lifecycle.json'], render: (root ) => renderBehaviourMatrix(inert(readJson(root, 'core/registry/lifecycle.json').coverage)) },
|
|
294
273
|
{ id: 'task-classes', page: 'how-doflow-work.md', sources: ['core/registry/workflows.json', 'core/registry/readiness-templates.json'], render: renderTaskClasses },
|
|
295
274
|
{ id: 'settings-keys', page: 'settings.md', sources: ['core/registry/settings.json'], render: renderSettingsKeys },
|
|
296
275
|
{ id: 'command-index', page: 'reference.md', sources: [DOFLOW_BIN, DOFLOW_RUN_BIN], render: renderCommandIndex },
|
|
276
|
+
{ id: 'intake-categories', page: 'guide.md', sources: ['core/registry/intake.json'], render: renderIntakeCategories },
|
|
297
277
|
]);
|
|
298
278
|
|
|
299
279
|
function renderBlock(id , root ) {
|
|
@@ -380,7 +360,7 @@ function renderLlms(root ) {
|
|
|
380
360
|
* (a lone BEGIN or END, an END before its BEGIN, a doubled pair) or nested inside another span is an
|
|
381
361
|
* orphan. Markers are whole lines only, so marker-shaped text inside a table row is never one.
|
|
382
362
|
*/
|
|
383
|
-
function markerFindings(page , text ) {
|
|
363
|
+
function markerFindings(page , text , blocks = BLOCKS) {
|
|
384
364
|
const markers = [...text.matchAll(MARKER_LIKE)].map((match) => ({ kind: match[1], id: match[2], index: match.index ?? 0, exact: match[0] === `<!-- ${match[1]} GENERATED:${match[2]} -->` }));
|
|
385
365
|
const bad = new Set (markers.filter((marker) => !marker.exact).map((marker) => marker.id));
|
|
386
366
|
const counts = new Map ();
|
|
@@ -401,7 +381,7 @@ function markerFindings(page , text ) {
|
|
|
401
381
|
if (open !== null) bad.add(open);
|
|
402
382
|
const findings = [];
|
|
403
383
|
for (const [id, { begin, end }] of counts) {
|
|
404
|
-
const registered = MARKER_ID.test(id) &&
|
|
384
|
+
const registered = MARKER_ID.test(id) && blocks.some((block) => block.id === id && block.page === page);
|
|
405
385
|
if (!registered || begin !== 1 || end !== 1 || bad.has(id)) findings.push({ page, id, kind: 'orphan' });
|
|
406
386
|
}
|
|
407
387
|
return findings;
|
|
@@ -422,15 +402,15 @@ function readDoc(root , page ) {
|
|
|
422
402
|
}
|
|
423
403
|
|
|
424
404
|
/** Every finding for the registered blocks and llms.txt; empty when the docs are current. */
|
|
425
|
-
function checkAll(root ) {
|
|
405
|
+
function checkAll(root , blocks = BLOCKS) {
|
|
426
406
|
const findings = [];
|
|
427
|
-
const pages = [...new Set(
|
|
407
|
+
const pages = [...new Set(blocks.map((block) => block.page))];
|
|
428
408
|
const texts = new Map(pages.map((page) => [page, readDoc(root, page)]));
|
|
429
409
|
for (const page of pages) {
|
|
430
410
|
const text = texts.get(page);
|
|
431
|
-
if (text !== null && text !== undefined) findings.push(...markerFindings(page, text));
|
|
411
|
+
if (text !== null && text !== undefined) findings.push(...markerFindings(page, text, blocks));
|
|
432
412
|
}
|
|
433
|
-
for (const block of
|
|
413
|
+
for (const block of blocks) {
|
|
434
414
|
const text = texts.get(block.page);
|
|
435
415
|
const span = typeof text === 'string' ? spanOf(text, block.id) : null;
|
|
436
416
|
if (!span) {
|
|
@@ -456,11 +436,11 @@ function checkAll(root ) {
|
|
|
456
436
|
}
|
|
457
437
|
|
|
458
438
|
/** Rewrites every well-formed span and llms.txt, then returns what `checkAll` still finds. */
|
|
459
|
-
function writeAll(root ) {
|
|
460
|
-
for (const block of
|
|
439
|
+
function writeAll(root , blocks = BLOCKS) {
|
|
440
|
+
for (const block of blocks) {
|
|
461
441
|
const file = path.join(root, 'docs', block.page);
|
|
462
442
|
const text = readDoc(root, block.page);
|
|
463
|
-
if (text === null || markerFindings(block.page, text).some((f) => f.id === block.id)) continue;
|
|
443
|
+
if (text === null || markerFindings(block.page, text, blocks).some((f) => f.id === block.id)) continue;
|
|
464
444
|
const span = spanOf(text, block.id);
|
|
465
445
|
if (!span) continue;
|
|
466
446
|
let rendered ;
|
|
@@ -472,7 +452,7 @@ function writeAll(root ) {
|
|
|
472
452
|
const llms = renderLlms(root);
|
|
473
453
|
if (readDoc(root, 'llms.txt') !== llms) fs.writeFileSync(path.join(root, 'docs', 'llms.txt'), llms);
|
|
474
454
|
} catch { /* reported by checkAll as source-unreadable */ }
|
|
475
|
-
return checkAll(root);
|
|
455
|
+
return checkAll(root, blocks);
|
|
476
456
|
}
|
|
477
457
|
|
|
478
458
|
/** The CLI: 0 current (or written clean), 1 findings, 2 usage. */
|
|
@@ -483,10 +463,14 @@ function main(argv , root = REPO_ROOT) {
|
|
|
483
463
|
}
|
|
484
464
|
const findings = argv[0] === '--write' ? writeAll(root) : checkAll(root);
|
|
485
465
|
for (const finding of findings) process.stderr.write(`docs-blocks: ${finding.page}: ${finding.id}: ${finding.kind}${finding.detail ? ` (${oneLine(finding.detail)})` : ''}\n`);
|
|
486
|
-
if (findings.length && argv[0] === '--check')
|
|
466
|
+
if (findings.length && argv[0] === '--check') {
|
|
467
|
+
process.stderr.write('docs-blocks: run node src/release/docs-blocks.js --write, then review the diff\n');
|
|
468
|
+
// --write rewrites llms.txt from the pages; when the rewritten text is itself over the cap, only shorter descriptions fix it.
|
|
469
|
+
if (findings.some((f) => f.kind === 'over-cap')) process.stderr.write(`docs-blocks: llms.txt over ${LLMS_CAP} bytes: if --write leaves it over, shorten the page descriptions it is built from\n`);
|
|
470
|
+
}
|
|
487
471
|
return findings.length ? 1 : 0;
|
|
488
472
|
}
|
|
489
473
|
|
|
490
|
-
module.exports = { BLOCKS, LLMS_CAP, MARKER_ID, markerFindings, text, code,
|
|
474
|
+
module.exports = { BLOCKS, LLMS_CAP, MARKER_ID, markerFindings, text, code, renderBlock, renderLlms, readNav, captureHelp, pageDescription, checkAll, writeAll, main };
|
|
491
475
|
|
|
492
476
|
if (require.main === module) process.exitCode = main(process.argv.slice(2));
|
|
@@ -116,12 +116,12 @@ async function buildAdviceDoctor({ repoRoot = REPO_ROOT, projectRoot = process.c
|
|
|
116
116
|
decisionModels: [], lastCall: latestCall(window.records),
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
return { decisionModel, advice, _inspection: inspection, _offReason: setting.state === 'off' ? offReason(setting) : null };
|
|
119
|
+
return { decisionModel, advice, _inspection: inspection, _offReason: setting.state === 'off' ? (loaded.ok ? offReason(setting) : `unreadable advice registry: ${loaded.detail}`) : null };
|
|
120
120
|
} catch (error ) {
|
|
121
121
|
return { decisionModel: { setting: { state: 'off', reason: 'config-invalid', decidedBy: { user: null, project: null }, modelFrom: null, notes: [error.message] },
|
|
122
122
|
endpoint: { url: null, state: 'not-checked', version: null, minVersion: null, versionOk: false, message: null },
|
|
123
123
|
model: { name: null, installed: false, decisionCapable: false }, decisionModels: [], lastCall: null },
|
|
124
|
-
advice: { windowDays: WINDOW_DAYS, store: 'advice-picks-user', kinds: [], unreadableLines: 0 }, _inspection: null, _offReason:
|
|
124
|
+
advice: { windowDays: WINDOW_DAYS, store: 'advice-picks-user', kinds: [], unreadableLines: 0 }, _inspection: null, _offReason: `advice doctor could not complete: ${error.message}` };
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -328,7 +328,7 @@ function adviceRecord(result ) {
|
|
|
328
328
|
/**
|
|
329
329
|
* The tier renderer remains synchronous/pure; optional local advice is requested afterward.
|
|
330
330
|
*/
|
|
331
|
-
async function handleBriefToolsCommand({ owner, tier, harness, slug, headingLevel, tasks, json = false, repoRoot = REPO_ROOT, projectRoot = process.cwd(), env = process.env, buildAdapters = null, probe = probeProviders, adviceClient = null } = {}) {
|
|
331
|
+
async function handleBriefToolsCommand({ owner, tier, harness, slug, headingLevel, tasks, json = false, repoRoot = REPO_ROOT, projectRoot = process.cwd(), env = process.env, buildAdapters = null, probe = probeProviders, adviceClient = null, now = () => new Date() } = {}) {
|
|
332
332
|
if (!owner) return usageError('brief-tools', '--owner is required (the agent type the brief is for)', json);
|
|
333
333
|
const level = headingLevel === undefined ? 2 : Number(headingLevel);
|
|
334
334
|
if (level !== 2 && level !== 3) return usageError('brief-tools', `--heading-level must be 2 or 3, got '${headingLevel}'`, json);
|
|
@@ -349,6 +349,7 @@ async function handleBriefToolsCommand({ owner, tier, harness, slug, headingLeve
|
|
|
349
349
|
let markdown = rendered.markdown;
|
|
350
350
|
const record = rendered.record;
|
|
351
351
|
record.missing.push(...notes);
|
|
352
|
+
record.createdAt = now().toISOString();
|
|
352
353
|
|
|
353
354
|
const taskIds = Array.isArray(tasks) ? tasks : typeof tasks === 'string' ? tasks.split(',').filter(Boolean) : [];
|
|
354
355
|
if (taskIds.length) {
|
|
@@ -298,6 +298,15 @@ class CapabilityRouter {
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** The registry's `capabilities.mcp.toolName` template for a harness id, or null when it has none. */
|
|
302
|
+
harnessToolTemplate(harnessId ) {
|
|
303
|
+
return this.harnessList().find((item ) => item.id === harnessId)?.capabilities?.mcp?.toolName || null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
harnessList() {
|
|
307
|
+
return parseYamlFile(path.join(this.registryDir, 'harnesses.json'), this.fsImpl).harnesses || [];
|
|
308
|
+
}
|
|
309
|
+
|
|
301
310
|
/**
|
|
302
311
|
* Formats concrete execution instructions for a provider based on intent parameters.
|
|
303
312
|
*
|
|
@@ -320,7 +329,17 @@ class CapabilityRouter {
|
|
|
320
329
|
// back to the registry entry with that id.
|
|
321
330
|
const mcp = provider.mcp
|
|
322
331
|
|| Object.values(this.capabilities).flatMap((cap ) => cap.providers || []).find((p ) => p.id === provider.id && p.mcp)?.mcp;
|
|
323
|
-
|
|
332
|
+
// Default (no `params.harness`): the Claude tool-name form, unchanged. With a target harness:
|
|
333
|
+
// that harness's registry `capabilities.mcp.toolName` template, or — when it has no verified
|
|
334
|
+
// template — the neutral `{server, tool}` identifiers and no tool name.
|
|
335
|
+
let mcpTool = {};
|
|
336
|
+
if (mcp && !params.harness) mcpTool = { mcpTool: `mcp__${mcp.server}__${mcp.tool}` };
|
|
337
|
+
else if (mcp) {
|
|
338
|
+
const template = this.harnessToolTemplate(params.harness);
|
|
339
|
+
mcpTool = template
|
|
340
|
+
? { mcpTool: template.replace('{server}', () => mcp.server).replace('{tool}', () => mcp.tool) }
|
|
341
|
+
: { mcp: { server: mcp.server, tool: mcp.tool } };
|
|
342
|
+
}
|
|
324
343
|
const withDisplay = (result ) => ({ ...mcpTool, ...result, ...(result.argv ? { cliCommand: result.argv.map(shellQuote).join(' ') } : {}) });
|
|
325
344
|
|
|
326
345
|
if (provider.id === 'semble.search') {
|
|
@@ -580,15 +599,19 @@ class CapabilityRouter {
|
|
|
580
599
|
* @param {string} [options.projectRoot]
|
|
581
600
|
* @returns {number} exit code
|
|
582
601
|
*/
|
|
583
|
-
function handleRouteCommand({ intent, query, check = false, json = false, projectRoot } = {}) {
|
|
602
|
+
function handleRouteCommand({ intent, query, check = false, json = false, projectRoot, harness } = {}) {
|
|
584
603
|
const router = new CapabilityRouter({ repoRoot: REPO_ROOT, projectRoot });
|
|
585
604
|
if (typeof intent !== 'string' || intent.trim() === '') {
|
|
586
605
|
return usageError('route', `--intent is required. Declared intents: ${Object.keys(router.routes).join(', ')}`, json);
|
|
587
606
|
}
|
|
588
607
|
|
|
608
|
+
if (harness !== undefined && !router.harnessList().some((item ) => item.id === harness)) {
|
|
609
|
+
return usageError('route', `unknown --harness '${harness}'. Known harnesses: ${router.harnessList().map((item ) => item.id).join(', ')}`, json);
|
|
610
|
+
}
|
|
611
|
+
|
|
589
612
|
let resolution ;
|
|
590
613
|
try {
|
|
591
|
-
resolution = router.resolveIntent(intent, { query, path: projectRoot || '.' }, { deepCheck: check });
|
|
614
|
+
resolution = router.resolveIntent(intent, { query, path: projectRoot || '.', ...(harness ? { harness } : {}) }, { deepCheck: check });
|
|
592
615
|
} catch (error ) {
|
|
593
616
|
return usageError('route', `${error.message}. Declared intents: ${Object.keys(router.routes).join(', ')}`, json, error);
|
|
594
617
|
}
|