@open-agent-toolkit/cli 0.3.9 → 0.3.11
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 +2 -1
- package/assets/bundle-metadata.json +1 -1
- package/assets/config/dispatch-matrix-recommendation.json +6 -6
- package/assets/docs/cli-utilities/configuration.md +12 -8
- package/assets/docs/cli-utilities/meta.json +13 -0
- package/assets/docs/cli-utilities/tool-packs.md +8 -2
- package/assets/docs/cli-utilities/workflow-gates.md +14 -1
- package/assets/docs/contributing/code.md +8 -2
- package/assets/docs/contributing/documentation.md +7 -1
- package/assets/docs/contributing/meta.json +15 -0
- package/assets/docs/contributing/updating-model-guidance.md +34 -6
- package/assets/docs/contributing/verifying-cursor-pins.md +38 -9
- package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -5
- package/assets/docs/docs-tooling/commands.md +26 -8
- package/assets/docs/docs-tooling/meta.json +4 -0
- package/assets/docs/docs-tooling/workflows.md +2 -2
- package/assets/docs/guide/meta.json +4 -0
- package/assets/docs/meta.json +14 -0
- package/assets/docs/provider-sync/instruction-sync.md +8 -0
- package/assets/docs/provider-sync/meta.json +11 -0
- package/assets/docs/provider-sync/providers.md +4 -4
- package/assets/docs/provider-sync/scope-and-surface.md +3 -4
- package/assets/docs/reference/cli-reference.md +5 -2
- package/assets/docs/reference/docs-index-contract.md +22 -6
- package/assets/docs/reference/file-locations.md +7 -2
- package/assets/docs/reference/index.md +1 -1
- package/assets/docs/reference/meta.json +10 -0
- package/assets/docs/reference/troubleshooting.md +41 -6
- package/assets/docs/workflows/ideas/meta.json +4 -0
- package/assets/docs/workflows/meta.json +4 -0
- package/assets/docs/workflows/projects/cursor-cloud.md +8 -3
- package/assets/docs/workflows/projects/dispatch-ceiling.md +21 -14
- package/assets/docs/workflows/projects/evidence-layers.md +6 -8
- package/assets/docs/workflows/projects/implementation-execution.md +34 -5
- package/assets/docs/workflows/projects/lifecycle.md +7 -1
- package/assets/docs/workflows/projects/meta.json +25 -0
- package/assets/docs/workflows/projects/orchestration-model.md +5 -6
- package/assets/docs/workflows/skills/meta.json +10 -0
- package/assets/docs/workflows/skills/recon.md +28 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-cursor-cloud-projects/SKILL.md +30 -16
- package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -12
- package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +1 -1
- package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +51 -0
- package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +5 -2
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +8 -7
- package/assets/skills/oat-docs-analyze/SKILL.md +28 -3
- package/assets/skills/oat-docs-apply/SKILL.md +3 -3
- package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
- package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +5 -2
- package/assets/skills/oat-docs-bootstrap/SKILL.md +4 -3
- package/assets/skills/oat-docs-bootstrap/assets/AGENTS.md.template +3 -3
- package/assets/skills/oat-pjm-decision/SKILL.md +3 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +4 -4
- package/assets/skills/oat-project-complete/SKILL.md +47 -2
- package/assets/skills/oat-project-design/SKILL.md +10 -6
- package/assets/skills/oat-project-discover/SKILL.md +3 -3
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +3 -10
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +135 -3
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +8 -10
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +1 -1
- package/assets/skills/oat-project-import-plan/SKILL.md +3 -3
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-new/SKILL.md +3 -3
- package/assets/skills/oat-project-next/SKILL.md +50 -7
- package/assets/skills/oat-project-plan/SKILL.md +4 -4
- package/assets/skills/oat-project-plan-writing/SKILL.md +4 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +11 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +12 -8
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-retro/SKILL.md +6 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +2 -2
- package/assets/skills/oat-project-spec/SKILL.md +3 -3
- package/assets/skills/oat-project-summary/SKILL.md +22 -3
- package/assets/skills/oat-repo-improve/SKILL.md +2 -2
- package/assets/skills/recon/SKILL.md +24 -3
- package/assets/skills/recon/references/packet-contract.md +36 -6
- package/assets/skills/recon/references/profiles.md +3 -0
- package/assets/skills/recon/references/worker-contract.md +16 -3
- package/assets/skills/recon/scripts/create-review-brief.mjs +17 -86
- package/assets/skills/recon/scripts/lib/contracts.mjs +158 -41
- package/assets/skills/recon/scripts/lib/review-binding.mjs +123 -0
- package/assets/skills/recon/scripts/lib/routing.mjs +5 -2
- package/assets/skills/recon/scripts/reconcile-ledger.mjs +18 -7
- package/assets/skills/recon/scripts/render-packet.mjs +93 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +119 -88
- package/assets/skills/subagent-orchestration/SKILL.md +1 -1
- package/assets/skills/subagent-orchestration/references/evidence-and-refresh.md +20 -8
- package/assets/skills/subagent-orchestration/references/provider-claude.md +25 -12
- package/assets/skills/subagent-orchestration/references/provider-codex.md +27 -18
- package/assets/skills/subagent-orchestration/references/provider-cursor.md +40 -10
- package/assets/templates/state.md +1 -0
- package/dist/commands/backlog/new.d.ts +2 -2
- package/dist/commands/backlog/new.d.ts.map +1 -1
- package/dist/commands/backlog/new.js +2 -2
- package/dist/commands/decision/new.d.ts +2 -2
- package/dist/commands/decision/new.d.ts.map +1 -1
- package/dist/commands/decision/new.js +2 -2
- package/dist/commands/docs/nav/contents.d.ts +9 -0
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +7 -2
- package/dist/commands/docs/nav/fumadocs.d.ts +51 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -0
- package/dist/commands/docs/nav/fumadocs.js +301 -0
- package/dist/commands/docs/nav/sync.d.ts +31 -1
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +163 -6
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/instructions/instructions.utils.d.ts +19 -0
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +89 -17
- package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
- package/dist/commands/instructions/sync/sync.js +49 -2
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +2 -2
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts +97 -0
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -0
- package/dist/commands/project/closeout-check/closeout-invariant.js +301 -0
- package/dist/commands/project/closeout-check/index.d.ts +38 -0
- package/dist/commands/project/closeout-check/index.d.ts.map +1 -0
- package/dist/commands/project/closeout-check/index.js +117 -0
- package/dist/commands/project/complete-state/index.d.ts +4 -0
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +11 -13
- package/dist/commands/project/dispatch/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch/index.js +6 -30
- package/dist/commands/project/dispatch/record.d.ts +9 -18
- package/dist/commands/project/dispatch/record.d.ts.map +1 -1
- package/dist/commands/project/dispatch/record.js +15 -212
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/new/scaffold.d.ts +0 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +19 -20
- package/dist/commands/project/promote/promote.d.ts +2 -2
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +13 -11
- package/dist/commands/shared/frontmatter.d.ts +21 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +70 -4
- package/dist/commands/shared/template-source.d.ts +29 -0
- package/dist/commands/shared/template-source.d.ts.map +1 -0
- package/dist/commands/{pjm → shared}/template-source.js +24 -7
- package/dist/commands/state/generate.js +2 -2
- package/dist/commands/template/index.d.ts +10 -0
- package/dist/commands/template/index.d.ts.map +1 -0
- package/dist/commands/template/index.js +87 -0
- package/dist/commands/template/resolve.d.ts +31 -0
- package/dist/commands/template/resolve.d.ts.map +1 -0
- package/dist/commands/template/resolve.js +86 -0
- package/dist/fs/io.d.ts +0 -41
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +2 -414
- package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -1
- package/dist/providers/claude/dispatch-envelope.js +1 -0
- package/dist/providers/claude/targets.d.ts +1 -0
- package/dist/providers/claude/targets.d.ts.map +1 -1
- package/dist/providers/claude/targets.js +3 -1
- package/dist/providers/codex/codec/shared.d.ts +3 -0
- package/dist/providers/codex/codec/shared.d.ts.map +1 -1
- package/dist/providers/codex/codec/shared.js +4 -0
- package/dist/providers/cursor/codec/catalog.d.ts +2 -2
- package/dist/providers/cursor/codec/catalog.d.ts.map +1 -1
- package/dist/providers/cursor/codec/catalog.js +45 -0
- package/dist/providers/cursor/codec/mapping-validation.d.ts +4 -0
- package/dist/providers/cursor/codec/mapping-validation.d.ts.map +1 -0
- package/dist/providers/cursor/codec/mapping-validation.js +25 -0
- package/dist/providers/cursor/codec/materialize.d.ts.map +1 -1
- package/dist/providers/cursor/codec/materialize.js +2 -11
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +2 -11
- package/dist/providers/identity/absolute-paths.d.ts +4 -13
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
- package/dist/providers/identity/absolute-paths.js +4 -17
- package/dist/providers/identity/generic-dispatch-record.d.ts +1 -1
- package/dist/providers/identity/generic-dispatch-record.js +2 -2
- package/dist/providers/identity/oat-dispatch-record.d.ts +4 -357
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
- package/dist/providers/identity/oat-dispatch-record.js +4 -236
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +12 -4
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +32 -6
- package/package.json +2 -2
- package/dist/commands/pjm/template-source.d.ts +0 -14
- package/dist/commands/pjm/template-source.d.ts.map +0 -1
|
@@ -122,23 +122,11 @@ export async function inspectManagedShim(claudePath, agentsPath, dependencies) {
|
|
|
122
122
|
detail: `unable to read AGENTS.md (${getErrorCode(error) ?? 'unknown error'})`,
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
const [realAgents, realClaude] = await Promise.all([
|
|
129
|
-
dependencies.realpath(agentsPath),
|
|
130
|
-
dependencies.realpath(claudePath),
|
|
131
|
-
]);
|
|
132
|
-
agentsResolvesToClaude = realAgents === realClaude;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
return {
|
|
136
|
-
kind: 'unreadable',
|
|
137
|
-
detail: `unable to resolve AGENTS.md (${getErrorCode(error) ?? 'unknown error'})`,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
125
|
+
const resolution = await agentsResolvesToClaude(agentsPath, agentsStats, claudeStats, dependencies);
|
|
126
|
+
if (typeof resolution === 'string') {
|
|
127
|
+
return { kind: 'unreadable', detail: resolution };
|
|
140
128
|
}
|
|
141
|
-
if (
|
|
129
|
+
if (resolution) {
|
|
142
130
|
return {
|
|
143
131
|
kind: 'unmanaged',
|
|
144
132
|
detail: 'AGENTS.md resolves to this CLAUDE.md, so CLAUDE.md holds the instructions ' +
|
|
@@ -184,6 +172,90 @@ export async function inspectManagedShim(claudePath, agentsPath, dependencies) {
|
|
|
184
172
|
detail: 'hand-written or modified CLAUDE.md; kept',
|
|
185
173
|
};
|
|
186
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether `agentsPath` resolves to the regular file `claudePath`: a hard link
|
|
177
|
+
* of it, or a symlink chain whose endpoint is that file or a hard link of it.
|
|
178
|
+
* Such a CLAUDE.md holds the only copy of the instructions behind that
|
|
179
|
+
* AGENTS.md. Both stats come from `lstat`, and a symlinked AGENTS.md is judged
|
|
180
|
+
* by the device and inode of its resolved endpoint, not by its realpath
|
|
181
|
+
* spelling: `pkg/AGENTS.md -> ../alias.md`, with `alias.md` hard-linked to
|
|
182
|
+
* CLAUDE.md, has a different realpath but reaches the same file.
|
|
183
|
+
*
|
|
184
|
+
* Callers pass a regular CLAUDE.md, so the ordinary `CLAUDE.md -> AGENTS.md`
|
|
185
|
+
* shim, where CLAUDE.md is the symlink, never reaches this check.
|
|
186
|
+
*
|
|
187
|
+
* Returns the answer, or the reason it could not be decided.
|
|
188
|
+
*/
|
|
189
|
+
async function agentsResolvesToClaude(agentsPath, agentsStats, claudeStats, dependencies) {
|
|
190
|
+
if (!agentsStats.isSymbolicLink()) {
|
|
191
|
+
return (agentsStats.dev === claudeStats.dev && agentsStats.ino === claudeStats.ino);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const endpoint = await dependencies.lstat(await dependencies.realpath(agentsPath));
|
|
195
|
+
return endpoint.dev === claudeStats.dev && endpoint.ino === claudeStats.ino;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
return `unable to resolve AGENTS.md (${getErrorCode(error) ?? 'unknown error'})`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The AGENTS.md files among `agentsPaths` that resolve to `claudePath`, the
|
|
203
|
+
* check that keeps `instructions sync --force` from overwriting the only copy
|
|
204
|
+
* of the instructions under a shim strategy. A regular CLAUDE.md is judged as
|
|
205
|
+
* the `none` classification judges it (hard link, or a symlink chain that
|
|
206
|
+
* ends at it); a CLAUDE.md that is itself a symlink counts only when an
|
|
207
|
+
* AGENTS.md chain passes through it, so the ordinary `CLAUDE.md -> AGENTS.md`
|
|
208
|
+
* shim is never mistaken for a link the other way.
|
|
209
|
+
*
|
|
210
|
+
* A missing CLAUDE.md or AGENTS.md links nothing. Any other read failure is
|
|
211
|
+
* returned as the reason the answer is unknown, and the caller keeps the file.
|
|
212
|
+
*/
|
|
213
|
+
export async function findAgentsResolvingTo(claudePath, agentsPaths, dependencies) {
|
|
214
|
+
let claudeStats;
|
|
215
|
+
try {
|
|
216
|
+
claudeStats = await dependencies.lstat(claudePath);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const errorCode = getErrorCode(error);
|
|
220
|
+
return errorCode === 'ENOENT'
|
|
221
|
+
? { linkers: [] }
|
|
222
|
+
: {
|
|
223
|
+
unreadable: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (claudeStats.isSymbolicLink()) {
|
|
227
|
+
return {
|
|
228
|
+
linkers: await findLinksThrough(claudePath, agentsPaths, dependencies),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (!claudeStats.isFile()) {
|
|
232
|
+
return { linkers: [] };
|
|
233
|
+
}
|
|
234
|
+
const linkers = [];
|
|
235
|
+
for (const agentsPath of new Set(agentsPaths)) {
|
|
236
|
+
let agentsStats;
|
|
237
|
+
try {
|
|
238
|
+
agentsStats = await dependencies.lstat(agentsPath);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorCode = getErrorCode(error);
|
|
242
|
+
if (errorCode === 'ENOENT') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
unreadable: `unable to read AGENTS.md (${errorCode ?? 'unknown error'})`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const resolution = await agentsResolvesToClaude(agentsPath, agentsStats, claudeStats, dependencies);
|
|
250
|
+
if (typeof resolution === 'string') {
|
|
251
|
+
return { unreadable: resolution };
|
|
252
|
+
}
|
|
253
|
+
if (resolution) {
|
|
254
|
+
linkers.push(agentsPath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { linkers: linkers.sort((left, right) => left.localeCompare(right)) };
|
|
258
|
+
}
|
|
187
259
|
/**
|
|
188
260
|
* The path of a directory entry itself, with its parent directories resolved
|
|
189
261
|
* but the entry not followed, so a link and the file it names compare by the
|
|
@@ -246,7 +318,7 @@ export async function findLinksThrough(targetPath, candidates, dependencies) {
|
|
|
246
318
|
return linkers.sort((left, right) => left.localeCompare(right));
|
|
247
319
|
}
|
|
248
320
|
/** `a` / `a and b` / `a, b, and c`, repository-relative. */
|
|
249
|
-
function describeLinkers(repoRoot, linkers) {
|
|
321
|
+
export function describeLinkers(repoRoot, linkers) {
|
|
250
322
|
const paths = linkers.map((path) => toPosixPath(relative(repoRoot, path)));
|
|
251
323
|
if (paths.length <= 2) {
|
|
252
324
|
return paths.join(' and ');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,EAAE,EAGH,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAKL,KAAK,mCAAmC,EAEzC,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,EAAE,EAGH,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAKL,KAAK,mCAAmC,EAEzC,MAAM,2CAA2C,CAAC;AAyBnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf;AA2lBD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CAkMT"}
|
|
@@ -2,7 +2,7 @@ import { lstat, readFile, readlink, realpath, rm, symlink, writeFile, } from 'no
|
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
4
4
|
import { INSTRUCTION_SYNC_STRATEGIES, } from '../../instructions/instructions.types.js';
|
|
5
|
-
import { buildInstructionsPayload, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, readConfiguredInstructionSyncStrategy, resolveInstructionPointerExcludes, resolveInstructionSyncStrategy, findLeftoverClaudeFiles, findLinksThrough, inspectManagedShim, buildLeftoverClaudeWarnings, buildShimRemovalBlockWarning, describeShimRemovalBlock, findShimRemovalBlockers, omitHeldBackLeftovers, markBlockedShimEntries, scanInstructionFiles, verifyManagedShimUnchanged, } from '../../instructions/instructions.utils.js';
|
|
5
|
+
import { buildInstructionsPayload, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, readConfiguredInstructionSyncStrategy, resolveInstructionPointerExcludes, resolveInstructionSyncStrategy, describeLinkers, findAgentsResolvingTo, findLeftoverClaudeFiles, findLinksThrough, inspectManagedShim, buildLeftoverClaudeWarnings, buildShimRemovalBlockWarning, describeShimRemovalBlock, findShimRemovalBlockers, omitHeldBackLeftovers, markBlockedShimEntries, scanInstructionFiles, verifyManagedShimUnchanged, } from '../../instructions/instructions.utils.js';
|
|
6
6
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
7
7
|
import { CliError } from '../../../errors/cli-error.js';
|
|
8
8
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
@@ -213,6 +213,50 @@ function planSyncActions({ entries, force, strategy, }) {
|
|
|
213
213
|
}
|
|
214
214
|
return actions;
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Under a shim strategy, keep every CLAUDE.md that an AGENTS.md resolves to
|
|
218
|
+
* (a symlink, a symlink chain, or a hard link) instead of overwriting it:
|
|
219
|
+
* that CLAUDE.md holds the only copy of the instructions behind the link.
|
|
220
|
+
* Planning-time only; a planned overwrite of any other file is unchanged.
|
|
221
|
+
*/
|
|
222
|
+
async function keepClaudeFilesAgentsResolveTo(actions, entries, dependencies, repoRoot) {
|
|
223
|
+
const claudePaths = new Set(entries.map((entry) => entry.claudePath));
|
|
224
|
+
const agentsPaths = entries.flatMap((entry) => entry.agentsPath === null || hasUnreadableCanonicalAgents(entry)
|
|
225
|
+
? []
|
|
226
|
+
: [entry.agentsPath]);
|
|
227
|
+
if (agentsPaths.length === 0) {
|
|
228
|
+
return actions;
|
|
229
|
+
}
|
|
230
|
+
const kept = [];
|
|
231
|
+
for (const action of actions) {
|
|
232
|
+
if (action.type !== 'update' || !claudePaths.has(action.target)) {
|
|
233
|
+
kept.push(action);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const resolution = await findAgentsResolvingTo(action.target, agentsPaths, dependencies);
|
|
237
|
+
if ('unreadable' in resolution) {
|
|
238
|
+
kept.push({
|
|
239
|
+
type: 'skip',
|
|
240
|
+
target: action.target,
|
|
241
|
+
reason: `cannot tell whether an AGENTS.md resolves to this CLAUDE.md (${resolution.unreadable}); kept`,
|
|
242
|
+
result: 'skipped',
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (resolution.linkers.length === 0) {
|
|
247
|
+
kept.push(action);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const verb = resolution.linkers.length === 1 ? 'resolves' : 'resolve';
|
|
251
|
+
kept.push({
|
|
252
|
+
type: 'skip',
|
|
253
|
+
target: action.target,
|
|
254
|
+
reason: `${describeLinkers(repoRoot, resolution.linkers)} ${verb} to this CLAUDE.md, so it holds the instructions; kept`,
|
|
255
|
+
result: 'skipped',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return kept;
|
|
259
|
+
}
|
|
216
260
|
async function applySyncActions(actions, entries, dependencies, strategy, repoRoot) {
|
|
217
261
|
const appliedActions = [];
|
|
218
262
|
const entriesByTarget = new Map();
|
|
@@ -454,11 +498,14 @@ export function createInstructionsSyncCommand(overrides = {}) {
|
|
|
454
498
|
? await dependencies.findLeftoverClaudeFiles(repoRoot)
|
|
455
499
|
: [];
|
|
456
500
|
const blockers = findShimRemovalBlockers(entries, leftoversBeforeSync);
|
|
457
|
-
const
|
|
501
|
+
const plannedOverwrites = planSyncActions({
|
|
458
502
|
entries,
|
|
459
503
|
force: options.force ?? false,
|
|
460
504
|
strategy,
|
|
461
505
|
});
|
|
506
|
+
const unblockedActions = strategy === 'none'
|
|
507
|
+
? plannedOverwrites
|
|
508
|
+
: await keepClaudeFilesAgentsResolveTo(plannedOverwrites, entries, dependencies, repoRoot);
|
|
462
509
|
const heldBack = unblockedActions
|
|
463
510
|
.filter((action) => action.type === 'remove')
|
|
464
511
|
.map((action) => action.target);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAC;AAKpC,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE;QACR,iBAAiB,EAAE,mBAAmB,CAAC;QACvC,SAAS,EAAE,mBAAmB,CAAC;KAChC,CAAC;CACH;AAsBD,eAAO,MAAM,sBAAsB,QAIoB,CAAC;AAOxD,eAAO,MAAM,gCAAgC,0FAC4C,CAAC;AAC1F,eAAO,MAAM,8BAA8B,QAG4C,CAAC;AACxF,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR;AAeD,eAAO,MAAM,8BAA8B,kVAIjC,CAAC;AAEX,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAiC7C,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAsHlC"}
|
|
@@ -6,8 +6,8 @@ import { initializeDecisionRecords } from '../decision/init.js';
|
|
|
6
6
|
import { buildProjectManagementAgentsSectionBody, PROJECT_MANAGEMENT_AGENTS_SECTION_KEY, } from '../init/tools/project-management/agents-guidance.js';
|
|
7
7
|
import { upsertAgentsMdSections, } from '../shared/agents-md.js';
|
|
8
8
|
import { stripTemplateFrontmatter } from '../shared/strip-template-frontmatter.js';
|
|
9
|
+
import { resolveTemplate } from '../shared/template-source.js';
|
|
9
10
|
import { readOatConfig, writeOatConfig } from '../../config/oat-config.js';
|
|
10
|
-
import { resolvePjmTemplate } from './template-source.js';
|
|
11
11
|
const TEMPLATE_TARGETS = [
|
|
12
12
|
{ template: 'repo-agents.md', target: 'AGENTS.md' },
|
|
13
13
|
{ template: 'pjm-agents.md', target: 'pjm/AGENTS.md' },
|
|
@@ -89,7 +89,7 @@ export async function initializeRepoReference(options) {
|
|
|
89
89
|
skipped.push(target.target);
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
|
-
const template = await
|
|
92
|
+
const template = await resolveTemplate({
|
|
93
93
|
name: target.template,
|
|
94
94
|
assetsRoot: options.assetsRoot,
|
|
95
95
|
templatesRoot: options.templatesRoot,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closeout invariant shared by `oat project closeout-check` and
|
|
3
|
+
* `oat project complete-state` (BL-260806-fail-closed-when-configured).
|
|
4
|
+
*
|
|
5
|
+
* A closeout that the implement skill must snapshot (configured
|
|
6
|
+
* `workflow.postImplementSequence`, autonomous, or lite) cannot reach terminal
|
|
7
|
+
* completion until its durable `oat_post_implement_sequence` snapshot exists,
|
|
8
|
+
* every stored step is recorded complete, approval is recorded, and the
|
|
9
|
+
* snapshot reached `status: complete`. The snapshot schema is the one
|
|
10
|
+
* `oat-project-implement/references/completion-and-closeout.md` Step 15
|
|
11
|
+
* persists. Once a snapshot exists it is authoritative: current configuration
|
|
12
|
+
* is never consulted, so a later config change cannot invalidate a persisted
|
|
13
|
+
* run.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SEQUENCE_FIELD = "oat_post_implement_sequence";
|
|
16
|
+
export declare const RECOVERY_SKILL = "oat-project-implement";
|
|
17
|
+
declare const POST_APPROVAL_STEPS: readonly ['summary', 'document', 'pr', 'retro'];
|
|
18
|
+
declare const SEQUENCE_STATUSES: readonly ['pre_approval', 'awaiting_approval', 'post_approval', 'failed', 'complete'];
|
|
19
|
+
declare const APPROVALS: readonly ['pending', 'approved', 'not_required'];
|
|
20
|
+
declare const SOURCES: readonly ['configured', 'autonomous-default'];
|
|
21
|
+
export type SequenceStep = (typeof POST_APPROVAL_STEPS)[number];
|
|
22
|
+
export type SequencePhase = 'pre_approval' | 'post_approval';
|
|
23
|
+
export declare const STEP_SKILLS: Record<SequenceStep, string>;
|
|
24
|
+
export interface CloseoutSnapshot {
|
|
25
|
+
status: (typeof SEQUENCE_STATUSES)[number];
|
|
26
|
+
source: (typeof SOURCES)[number] | null;
|
|
27
|
+
preApproval: SequenceStep[];
|
|
28
|
+
preApprovalCompleted: SequenceStep[];
|
|
29
|
+
approval: (typeof APPROVALS)[number];
|
|
30
|
+
postApproval: SequenceStep[];
|
|
31
|
+
postApprovalCompleted: SequenceStep[];
|
|
32
|
+
failure: unknown;
|
|
33
|
+
}
|
|
34
|
+
export type CloseoutInvariant = 'snapshot_missing' | 'snapshot_malformed' | 'sequence_failed' | 'pre_approval_step_pending' | 'approval_pending' | 'post_approval_step_pending' | 'sequence_not_complete';
|
|
35
|
+
export type CloseoutNextOwner = {
|
|
36
|
+
kind: 'snapshot';
|
|
37
|
+
skill: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'step';
|
|
40
|
+
phase: SequencePhase;
|
|
41
|
+
step: SequenceStep;
|
|
42
|
+
skill: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'approval';
|
|
45
|
+
skill: string;
|
|
46
|
+
/**
|
|
47
|
+
* The two valid approval writes: `approved` after final HiLL sign-off,
|
|
48
|
+
* `not_required` when no final checkpoint exists (Step 15 item 5).
|
|
49
|
+
*/
|
|
50
|
+
writes: readonly ['approval: approved', 'approval: not_required'];
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'sequence-status';
|
|
53
|
+
skill: string;
|
|
54
|
+
};
|
|
55
|
+
export type SnapshotRequirementReason = 'lite' | 'autonomous' | 'configured';
|
|
56
|
+
export interface CloseoutInputs {
|
|
57
|
+
workflowMode: string;
|
|
58
|
+
lite: boolean;
|
|
59
|
+
autonomous: boolean;
|
|
60
|
+
autonomousSource: 'flag' | 'env' | null;
|
|
61
|
+
/** Null when configuration was not consulted (snapshot present, or lite/autonomous already decided). */
|
|
62
|
+
configured: boolean | null;
|
|
63
|
+
snapshot: 'absent' | 'present';
|
|
64
|
+
snapshotSource: string | null;
|
|
65
|
+
}
|
|
66
|
+
export type CloseoutResult = {
|
|
67
|
+
status: 'complete';
|
|
68
|
+
inputs: CloseoutInputs;
|
|
69
|
+
} | {
|
|
70
|
+
status: 'not_required';
|
|
71
|
+
inputs: CloseoutInputs;
|
|
72
|
+
} | {
|
|
73
|
+
status: 'incomplete';
|
|
74
|
+
invariant: CloseoutInvariant;
|
|
75
|
+
detail: string;
|
|
76
|
+
route: typeof RECOVERY_SKILL;
|
|
77
|
+
nextOwner: CloseoutNextOwner;
|
|
78
|
+
inputs: CloseoutInputs;
|
|
79
|
+
};
|
|
80
|
+
export interface EvaluateCloseoutOptions {
|
|
81
|
+
stateContent: string;
|
|
82
|
+
autonomousFlag: boolean;
|
|
83
|
+
env: NodeJS.ProcessEnv;
|
|
84
|
+
/** Resolves the effective layered `workflow.postImplementSequence`; called only when needed. */
|
|
85
|
+
resolveConfigured: () => Promise<boolean>;
|
|
86
|
+
}
|
|
87
|
+
export declare function parseCloseoutSnapshot(value: unknown): CloseoutSnapshot;
|
|
88
|
+
export declare function evaluateCloseout(options: EvaluateCloseoutOptions): Promise<CloseoutResult>;
|
|
89
|
+
/**
|
|
90
|
+
* The one refusal message both commands print, so a skill or operator sees the
|
|
91
|
+
* same invariant and recovery route from either.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatCloseoutRefusal(projectPath: string, result: Extract<CloseoutResult, {
|
|
94
|
+
status: 'incomplete';
|
|
95
|
+
}>): string;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=closeout-invariant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closeout-invariant.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/closeout-check/closeout-invariant.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import YAML, { isMap } from 'yaml';
|
|
2
|
+
/**
|
|
3
|
+
* The closeout invariant shared by `oat project closeout-check` and
|
|
4
|
+
* `oat project complete-state` (BL-260806-fail-closed-when-configured).
|
|
5
|
+
*
|
|
6
|
+
* A closeout that the implement skill must snapshot (configured
|
|
7
|
+
* `workflow.postImplementSequence`, autonomous, or lite) cannot reach terminal
|
|
8
|
+
* completion until its durable `oat_post_implement_sequence` snapshot exists,
|
|
9
|
+
* every stored step is recorded complete, approval is recorded, and the
|
|
10
|
+
* snapshot reached `status: complete`. The snapshot schema is the one
|
|
11
|
+
* `oat-project-implement/references/completion-and-closeout.md` Step 15
|
|
12
|
+
* persists. Once a snapshot exists it is authoritative: current configuration
|
|
13
|
+
* is never consulted, so a later config change cannot invalidate a persisted
|
|
14
|
+
* run.
|
|
15
|
+
*/
|
|
16
|
+
export const SEQUENCE_FIELD = 'oat_post_implement_sequence';
|
|
17
|
+
export const RECOVERY_SKILL = 'oat-project-implement';
|
|
18
|
+
const APPROVAL_WRITES = [
|
|
19
|
+
'approval: approved',
|
|
20
|
+
'approval: not_required',
|
|
21
|
+
];
|
|
22
|
+
const PRE_APPROVAL_STEPS = ['summary', 'document', 'pr'];
|
|
23
|
+
const POST_APPROVAL_STEPS = ['summary', 'document', 'pr', 'retro'];
|
|
24
|
+
const SEQUENCE_STATUSES = [
|
|
25
|
+
'pre_approval',
|
|
26
|
+
'awaiting_approval',
|
|
27
|
+
'post_approval',
|
|
28
|
+
'failed',
|
|
29
|
+
'complete',
|
|
30
|
+
];
|
|
31
|
+
const APPROVALS = ['pending', 'approved', 'not_required'];
|
|
32
|
+
const SOURCES = ['configured', 'autonomous-default'];
|
|
33
|
+
const APPROVAL_SOURCES = ['user', 'oat-autonomous'];
|
|
34
|
+
export const STEP_SKILLS = {
|
|
35
|
+
summary: 'oat-project-summary',
|
|
36
|
+
document: 'oat-project-document',
|
|
37
|
+
pr: 'oat-project-pr-final',
|
|
38
|
+
retro: 'oat-project-retro',
|
|
39
|
+
};
|
|
40
|
+
class MalformedSnapshotError extends Error {
|
|
41
|
+
}
|
|
42
|
+
function malformed(detail) {
|
|
43
|
+
throw new MalformedSnapshotError(detail);
|
|
44
|
+
}
|
|
45
|
+
function readFrontmatter(content) {
|
|
46
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
47
|
+
if (!match) {
|
|
48
|
+
throw new Error('state.md is missing frontmatter');
|
|
49
|
+
}
|
|
50
|
+
const document = YAML.parseDocument(match[1], { uniqueKeys: true });
|
|
51
|
+
if (document.errors.length > 0) {
|
|
52
|
+
throw new Error(`state.md frontmatter is malformed (${document.errors[0]?.message ?? 'YAML parse error'})`);
|
|
53
|
+
}
|
|
54
|
+
if (!isMap(document.contents)) {
|
|
55
|
+
throw new Error('state.md frontmatter root is not a YAML map');
|
|
56
|
+
}
|
|
57
|
+
const value = document.toJS();
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function readEnum(record, key, allowed, options = {}) {
|
|
61
|
+
if (!(key in record)) {
|
|
62
|
+
if (options.optional)
|
|
63
|
+
return null;
|
|
64
|
+
malformed(`\`${key}\` is missing`);
|
|
65
|
+
}
|
|
66
|
+
const value = record[key];
|
|
67
|
+
if (value === null && options.nullable)
|
|
68
|
+
return null;
|
|
69
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
70
|
+
malformed(`\`${key}\` must be one of ${allowed.join(', ')}${options.nullable ? ', or null' : ''}`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function readSteps(record, key, allowed) {
|
|
75
|
+
const value = record[key];
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
malformed(`\`${key}\` must be an array`);
|
|
78
|
+
}
|
|
79
|
+
const steps = [];
|
|
80
|
+
for (const entry of value) {
|
|
81
|
+
if (typeof entry !== 'string' || !allowed.includes(entry)) {
|
|
82
|
+
malformed(`\`${key}\` contains \`${String(entry)}\`; allowed steps are ${allowed.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
if (steps.includes(entry)) {
|
|
85
|
+
malformed(`\`${key}\` lists \`${entry}\` more than once`);
|
|
86
|
+
}
|
|
87
|
+
steps.push(entry);
|
|
88
|
+
}
|
|
89
|
+
return steps;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Completed steps must be exactly the first N stored steps: the implement
|
|
93
|
+
* skill dispatches in stored order, so a gap or a foreign step is a
|
|
94
|
+
* contradiction, not progress.
|
|
95
|
+
*/
|
|
96
|
+
function assertCompletedPrefix(stored, completed, storedKey, completedKey) {
|
|
97
|
+
for (const step of completed) {
|
|
98
|
+
if (!stored.includes(step)) {
|
|
99
|
+
malformed(`\`${completedKey}\` lists \`${step}\`, which \`${storedKey}\` does not`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const prefix = stored.slice(0, completed.length);
|
|
103
|
+
if (!prefix.every((step) => completed.includes(step))) {
|
|
104
|
+
malformed(`\`${completedKey}\` is not a prefix of the stored \`${storedKey}\` order`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function parseCloseoutSnapshot(value) {
|
|
108
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
109
|
+
malformed('the snapshot must be a map');
|
|
110
|
+
}
|
|
111
|
+
const record = value;
|
|
112
|
+
const status = readEnum(record, 'status', SEQUENCE_STATUSES);
|
|
113
|
+
// `source` and `approval_source` are additive provenance; an older snapshot
|
|
114
|
+
// without them stays valid.
|
|
115
|
+
const source = readEnum(record, 'source', SOURCES, {
|
|
116
|
+
optional: true,
|
|
117
|
+
nullable: true,
|
|
118
|
+
});
|
|
119
|
+
readEnum(record, 'approval_source', APPROVAL_SOURCES, {
|
|
120
|
+
optional: true,
|
|
121
|
+
nullable: true,
|
|
122
|
+
});
|
|
123
|
+
if ('final_phase' in record &&
|
|
124
|
+
record.final_phase !== null &&
|
|
125
|
+
typeof record.final_phase !== 'string') {
|
|
126
|
+
malformed('`final_phase` must be a phase ID string');
|
|
127
|
+
}
|
|
128
|
+
const preApproval = readSteps(record, 'pre_approval', PRE_APPROVAL_STEPS);
|
|
129
|
+
const postApproval = readSteps(record, 'post_approval', POST_APPROVAL_STEPS);
|
|
130
|
+
for (const step of postApproval) {
|
|
131
|
+
if (preApproval.includes(step)) {
|
|
132
|
+
malformed(`\`${step}\` appears in both pre_approval and post_approval`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const preApprovalCompleted = readSteps(record, 'pre_approval_completed', PRE_APPROVAL_STEPS);
|
|
136
|
+
const postApprovalCompleted = readSteps(record, 'post_approval_completed', POST_APPROVAL_STEPS);
|
|
137
|
+
assertCompletedPrefix(preApproval, preApprovalCompleted, 'pre_approval', 'pre_approval_completed');
|
|
138
|
+
assertCompletedPrefix(postApproval, postApprovalCompleted, 'post_approval', 'post_approval_completed');
|
|
139
|
+
const approval = readEnum(record, 'approval', APPROVALS);
|
|
140
|
+
if (postApprovalCompleted.length > 0 && approval === 'pending') {
|
|
141
|
+
malformed('`post_approval_completed` records work while `approval` is still pending');
|
|
142
|
+
}
|
|
143
|
+
const snapshot = {
|
|
144
|
+
status,
|
|
145
|
+
source,
|
|
146
|
+
preApproval,
|
|
147
|
+
preApprovalCompleted,
|
|
148
|
+
approval,
|
|
149
|
+
postApproval,
|
|
150
|
+
postApprovalCompleted,
|
|
151
|
+
failure: record.failure ?? null,
|
|
152
|
+
};
|
|
153
|
+
if (status === 'complete') {
|
|
154
|
+
const pending = firstPending(preApproval, preApprovalCompleted) ??
|
|
155
|
+
firstPending(postApproval, postApprovalCompleted);
|
|
156
|
+
if (pending || approval === 'pending') {
|
|
157
|
+
malformed('`status: complete` contradicts pending steps or a pending approval');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return snapshot;
|
|
161
|
+
}
|
|
162
|
+
function firstPending(stored, completed) {
|
|
163
|
+
return stored.find((step) => !completed.includes(step));
|
|
164
|
+
}
|
|
165
|
+
function stepOwner(phase, step) {
|
|
166
|
+
return { kind: 'step', phase, step, skill: STEP_SKILLS[step] };
|
|
167
|
+
}
|
|
168
|
+
function approvalOwner() {
|
|
169
|
+
return { kind: 'approval', skill: RECOVERY_SKILL, writes: APPROVAL_WRITES };
|
|
170
|
+
}
|
|
171
|
+
function evaluateSnapshot(snapshot, inputs) {
|
|
172
|
+
const prePending = firstPending(snapshot.preApproval, snapshot.preApprovalCompleted);
|
|
173
|
+
const postPending = firstPending(snapshot.postApproval, snapshot.postApprovalCompleted);
|
|
174
|
+
const incomplete = (invariant, detail, nextOwner) => ({
|
|
175
|
+
status: 'incomplete',
|
|
176
|
+
invariant,
|
|
177
|
+
detail,
|
|
178
|
+
route: RECOVERY_SKILL,
|
|
179
|
+
nextOwner,
|
|
180
|
+
inputs,
|
|
181
|
+
});
|
|
182
|
+
if (snapshot.status === 'failed') {
|
|
183
|
+
// Approval-aware order: pending pre-approval work, then the pending
|
|
184
|
+
// approval boundary, then post-approval work, then status repair. A
|
|
185
|
+
// post-approval step is never named while approval is pending.
|
|
186
|
+
const approvalPending = snapshot.approval === 'pending';
|
|
187
|
+
const owner = prePending
|
|
188
|
+
? stepOwner('pre_approval', prePending)
|
|
189
|
+
: approvalPending
|
|
190
|
+
? approvalOwner()
|
|
191
|
+
: postPending
|
|
192
|
+
? stepOwner('post_approval', postPending)
|
|
193
|
+
: { kind: 'sequence-status', skill: RECOVERY_SKILL };
|
|
194
|
+
const at = owner.kind === 'step'
|
|
195
|
+
? ` at \`${owner.step}\``
|
|
196
|
+
: owner.kind === 'approval'
|
|
197
|
+
? ' at the approval boundary'
|
|
198
|
+
: '';
|
|
199
|
+
return incomplete('sequence_failed', `the post-implementation sequence recorded a failure${at}`, owner);
|
|
200
|
+
}
|
|
201
|
+
if (prePending) {
|
|
202
|
+
return incomplete('pre_approval_step_pending', `pre-approval step \`${prePending}\` is not recorded complete (stored order: ${snapshot.preApproval.join(', ')})`, stepOwner('pre_approval', prePending));
|
|
203
|
+
}
|
|
204
|
+
if (snapshot.approval === 'pending') {
|
|
205
|
+
return incomplete('approval_pending', 'final approval is not recorded', approvalOwner());
|
|
206
|
+
}
|
|
207
|
+
if (postPending) {
|
|
208
|
+
return incomplete('post_approval_step_pending', `post-approval step \`${postPending}\` is not recorded complete (stored order: ${snapshot.postApproval.join(', ')})`, stepOwner('post_approval', postPending));
|
|
209
|
+
}
|
|
210
|
+
if (snapshot.status !== 'complete') {
|
|
211
|
+
return incomplete('sequence_not_complete', `every step and approval is recorded but the snapshot status is \`${snapshot.status}\`, not \`complete\``, { kind: 'sequence-status', skill: RECOVERY_SKILL });
|
|
212
|
+
}
|
|
213
|
+
return { status: 'complete', inputs };
|
|
214
|
+
}
|
|
215
|
+
export async function evaluateCloseout(options) {
|
|
216
|
+
const frontmatter = readFrontmatter(options.stateContent);
|
|
217
|
+
const workflowMode = typeof frontmatter.oat_workflow_mode === 'string'
|
|
218
|
+
? frontmatter.oat_workflow_mode
|
|
219
|
+
: 'spec-driven';
|
|
220
|
+
const autonomousSource = options.autonomousFlag
|
|
221
|
+
? 'flag'
|
|
222
|
+
: options.env.OAT_AUTONOMOUS === '1'
|
|
223
|
+
? 'env'
|
|
224
|
+
: null;
|
|
225
|
+
const rawSnapshot = frontmatter[SEQUENCE_FIELD];
|
|
226
|
+
// An explicit null carries no snapshot, exactly like an absent key.
|
|
227
|
+
const snapshotPresent = SEQUENCE_FIELD in frontmatter && rawSnapshot !== null;
|
|
228
|
+
const inputs = {
|
|
229
|
+
workflowMode,
|
|
230
|
+
lite: workflowMode === 'lite',
|
|
231
|
+
autonomous: autonomousSource !== null,
|
|
232
|
+
autonomousSource,
|
|
233
|
+
configured: null,
|
|
234
|
+
snapshot: snapshotPresent ? 'present' : 'absent',
|
|
235
|
+
snapshotSource: null,
|
|
236
|
+
};
|
|
237
|
+
if (snapshotPresent) {
|
|
238
|
+
let snapshot;
|
|
239
|
+
try {
|
|
240
|
+
snapshot = parseCloseoutSnapshot(rawSnapshot);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (!(error instanceof MalformedSnapshotError))
|
|
244
|
+
throw error;
|
|
245
|
+
return {
|
|
246
|
+
status: 'incomplete',
|
|
247
|
+
invariant: 'snapshot_malformed',
|
|
248
|
+
detail: `\`${SEQUENCE_FIELD}\` is malformed: ${error.message}`,
|
|
249
|
+
route: RECOVERY_SKILL,
|
|
250
|
+
nextOwner: { kind: 'snapshot', skill: RECOVERY_SKILL },
|
|
251
|
+
inputs,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
inputs.snapshotSource = snapshot.source;
|
|
255
|
+
return evaluateSnapshot(snapshot, inputs);
|
|
256
|
+
}
|
|
257
|
+
let reason = null;
|
|
258
|
+
if (inputs.lite) {
|
|
259
|
+
reason = 'lite';
|
|
260
|
+
}
|
|
261
|
+
else if (inputs.autonomous) {
|
|
262
|
+
reason = 'autonomous';
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
inputs.configured = await options.resolveConfigured();
|
|
266
|
+
if (inputs.configured)
|
|
267
|
+
reason = 'configured';
|
|
268
|
+
}
|
|
269
|
+
if (!reason) {
|
|
270
|
+
return { status: 'not_required', inputs };
|
|
271
|
+
}
|
|
272
|
+
const why = {
|
|
273
|
+
lite: '`oat_workflow_mode: lite` always snapshots its closeout',
|
|
274
|
+
autonomous: 'an autonomous closeout (`--autonomous` or `OAT_AUTONOMOUS=1`) always snapshots its closeout',
|
|
275
|
+
configured: 'the effective `workflow.postImplementSequence` is configured',
|
|
276
|
+
}[reason];
|
|
277
|
+
return {
|
|
278
|
+
status: 'incomplete',
|
|
279
|
+
invariant: 'snapshot_missing',
|
|
280
|
+
detail: `\`${SEQUENCE_FIELD}\` is absent, but ${why}`,
|
|
281
|
+
route: RECOVERY_SKILL,
|
|
282
|
+
nextOwner: { kind: 'snapshot', skill: RECOVERY_SKILL },
|
|
283
|
+
inputs,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The one refusal message both commands print, so a skill or operator sees the
|
|
288
|
+
* same invariant and recovery route from either.
|
|
289
|
+
*/
|
|
290
|
+
export function formatCloseoutRefusal(projectPath, result) {
|
|
291
|
+
const owner = result.nextOwner;
|
|
292
|
+
const next = owner.kind === 'step'
|
|
293
|
+
? ` Next stored step: \`${owner.step}\` (${owner.skill}).`
|
|
294
|
+
: owner.kind === 'approval'
|
|
295
|
+
? ' Next: record the final approval decision: `approval: approved` after final HiLL sign-off, or `approval: not_required` when no final checkpoint exists.'
|
|
296
|
+
: '';
|
|
297
|
+
return [
|
|
298
|
+
`Closeout invariant not satisfied for ${projectPath} (${result.invariant}): ${result.detail}.${next}`,
|
|
299
|
+
`Resume with ${RECOVERY_SKILL}; it persists the ${SEQUENCE_FIELD} snapshot (completion-and-closeout.md Step 15) and completes every stored step before completion.`,
|
|
300
|
+
].join(' ');
|
|
301
|
+
}
|