@open-agent-toolkit/cli 0.2.50 → 0.2.52
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/bootstrap.md +6 -2
- package/assets/docs/cli-utilities/tool-packs.md +87 -33
- package/assets/docs/provider-sync/commands.md +30 -2
- package/assets/docs/provider-sync/config.md +19 -0
- package/assets/docs/provider-sync/manifest-and-drift.md +37 -0
- package/assets/docs/provider-sync/providers.md +69 -2
- package/assets/docs/provider-sync/scope-and-surface.md +44 -0
- package/assets/docs/reference/cli-reference.md +21 -5
- package/assets/docs/reference/file-locations.md +8 -2
- package/assets/docs/reference/oat-directory-structure.md +25 -9
- package/assets/docs/workflows/projects/artifacts.md +9 -4
- package/assets/docs/workflows/projects/evidence-layers.md +110 -1
- package/assets/docs/workflows/projects/implementation-execution.md +13 -0
- package/assets/docs/workflows/projects/lifecycle.md +17 -5
- package/assets/docs/workflows/projects/orchestration-model.md +18 -0
- package/assets/docs/workflows/projects/picking-up-projects.md +26 -9
- package/assets/docs/workflows/projects/pr-flow.md +5 -3
- package/assets/docs/workflows/projects/reviewing-oat-prs.md +12 -8
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +57 -1
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +77 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -3
- package/assets/skills/oat-project-complete/SKILL.md +193 -26
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +306 -0
- package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +106 -0
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +110 -0
- package/assets/skills/oat-project-complete/scripts/resolve-synced-archive-entry.mjs +164 -0
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +24 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -3
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -3
- package/assets/skills/oat-project-plan-writing/SKILL.md +12 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -3
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +12 -1
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +12 -1
- package/dist/agents/canonical/index.d.ts +2 -0
- package/dist/agents/canonical/index.d.ts.map +1 -1
- package/dist/agents/canonical/index.js +1 -0
- package/dist/agents/canonical/resolve.d.ts +45 -0
- package/dist/agents/canonical/resolve.d.ts.map +1 -0
- package/dist/agents/canonical/resolve.js +175 -0
- package/dist/commands/decision/agents-guidance.d.ts +2 -2
- package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
- package/dist/commands/decision/agents-guidance.js +1 -4
- package/dist/commands/decision/index.d.ts.map +1 -1
- package/dist/commands/decision/index.js +25 -6
- package/dist/commands/docs/init/index.d.ts +7 -1
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +53 -20
- package/dist/commands/doctor/index.d.ts +8 -2
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +157 -46
- package/dist/commands/init/index.d.ts +8 -3
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +64 -26
- package/dist/commands/init/tools/index.d.ts +12 -12
- package/dist/commands/init/tools/index.d.ts.map +1 -1
- package/dist/commands/init/tools/index.js +430 -99
- package/dist/commands/init/tools/install-state.d.ts +2 -0
- package/dist/commands/init/tools/install-state.d.ts.map +1 -1
- package/dist/commands/init/tools/install-state.js +11 -0
- package/dist/commands/init/tools/project-guidance.d.ts +49 -0
- package/dist/commands/init/tools/project-guidance.d.ts.map +1 -0
- package/dist/commands/init/tools/project-guidance.js +144 -0
- package/dist/commands/init/tools/workflows/index.d.ts +7 -0
- package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
- package/dist/commands/init/tools/workflows/index.js +160 -7
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +28 -3
- package/dist/commands/pjm/init.d.ts +5 -0
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +19 -3
- package/dist/commands/project/archive/archive-utils.d.ts +8 -1
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +147 -50
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +15 -2
- package/dist/commands/project/dispatch/index.d.ts +10 -0
- package/dist/commands/project/dispatch/index.d.ts.map +1 -0
- package/dist/commands/project/dispatch/index.js +137 -0
- package/dist/commands/project/dispatch/record.d.ts +91 -0
- package/dist/commands/project/dispatch/record.d.ts.map +1 -0
- package/dist/commands/project/dispatch/record.js +402 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/links/compute.d.ts +1 -0
- package/dist/commands/project/links/compute.d.ts.map +1 -1
- package/dist/commands/project/links/compute.js +5 -4
- package/dist/commands/project/links/index.d.ts +9 -0
- package/dist/commands/project/links/index.d.ts.map +1 -1
- package/dist/commands/project/links/index.js +71 -2
- package/dist/commands/project/list.d.ts +10 -2
- package/dist/commands/project/list.d.ts.map +1 -1
- package/dist/commands/project/list.js +205 -29
- package/dist/commands/project/open/index.d.ts +2 -0
- package/dist/commands/project/open/index.d.ts.map +1 -1
- package/dist/commands/project/open/index.js +16 -1
- package/dist/commands/project/prune/index.d.ts +10 -4
- package/dist/commands/project/prune/index.d.ts.map +1 -1
- package/dist/commands/project/prune/index.js +85 -15
- package/dist/commands/project/pull/index.d.ts +4 -0
- package/dist/commands/project/pull/index.d.ts.map +1 -1
- package/dist/commands/project/pull/index.js +82 -3
- package/dist/commands/project/sync/git.d.ts +9 -0
- package/dist/commands/project/sync/git.d.ts.map +1 -1
- package/dist/commands/project/sync/git.js +18 -0
- package/dist/commands/project/sync/ref-sync.d.ts +16 -1
- package/dist/commands/project/sync/ref-sync.d.ts.map +1 -1
- package/dist/commands/project/sync/ref-sync.js +288 -17
- package/dist/commands/project/sync/resolve-target.d.ts +10 -0
- package/dist/commands/project/sync/resolve-target.d.ts.map +1 -1
- package/dist/commands/project/sync/resolve-target.js +56 -1
- package/dist/commands/providers/inspect/inspect.d.ts +8 -1
- package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
- package/dist/commands/providers/inspect/inspect.js +146 -26
- package/dist/commands/providers/list/list.d.ts +8 -1
- package/dist/commands/providers/list/list.d.ts.map +1 -1
- package/dist/commands/providers/list/list.js +139 -23
- package/dist/commands/providers/providers.types.d.ts +25 -1
- package/dist/commands/providers/providers.types.d.ts.map +1 -1
- package/dist/commands/providers/set/index.d.ts +9 -1
- package/dist/commands/providers/set/index.d.ts.map +1 -1
- package/dist/commands/providers/set/index.js +43 -33
- package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
- package/dist/commands/remove/skill/remove-skill.js +11 -0
- package/dist/commands/shared/agents-md.d.ts +42 -14
- package/dist/commands/shared/agents-md.d.ts.map +1 -1
- package/dist/commands/shared/agents-md.js +364 -70
- package/dist/commands/shared/project-scope.d.ts +2 -0
- package/dist/commands/shared/project-scope.d.ts.map +1 -1
- package/dist/commands/shared/project-scope.js +5 -0
- package/dist/commands/state/generate.d.ts +2 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +93 -28
- package/dist/commands/status/index.d.ts +8 -3
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +147 -32
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +220 -29
- package/dist/commands/sync/dry-run.d.ts.map +1 -1
- package/dist/commands/sync/dry-run.js +30 -6
- package/dist/commands/sync/index.d.ts +17 -2
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +106 -41
- package/dist/commands/sync/sync.types.d.ts +39 -2
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.d.ts +5 -1
- package/dist/commands/sync/sync.utils.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.js +73 -0
- package/dist/commands/tools/info/info-tool.d.ts +2 -0
- package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
- package/dist/commands/tools/info/info-tool.js +34 -12
- package/dist/commands/tools/install/index.d.ts.map +1 -1
- package/dist/commands/tools/install/index.js +19 -3
- package/dist/commands/tools/list/list-tools.d.ts +2 -0
- package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
- package/dist/commands/tools/list/list-tools.js +36 -7
- package/dist/commands/tools/remove/index.d.ts.map +1 -1
- package/dist/commands/tools/remove/index.js +150 -21
- package/dist/commands/tools/remove/remove-tools.d.ts +20 -0
- package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
- package/dist/commands/tools/remove/remove-tools.js +122 -1
- package/dist/commands/tools/shared/format-pack-inventory.d.ts +19 -0
- package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/format-pack-inventory.js +109 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts +66 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-evidence.js +132 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts +1 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +44 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.js +91 -0
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +74 -4
- package/dist/commands/tools/update/update-tools.d.ts +5 -0
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.js +64 -2
- package/dist/config/oat-config.d.ts +5 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +17 -5
- package/dist/config/user-sync-config.d.ts +1 -0
- package/dist/config/user-sync-config.d.ts.map +1 -1
- package/dist/config/user-sync-config.js +8 -0
- package/dist/drift/detector.d.ts +2 -2
- package/dist/drift/detector.d.ts.map +1 -1
- package/dist/drift/detector.js +18 -0
- package/dist/drift/strays.d.ts.map +1 -1
- package/dist/drift/strays.js +5 -0
- package/dist/engine/collection-sync.d.ts +14 -0
- package/dist/engine/collection-sync.d.ts.map +1 -0
- package/dist/engine/collection-sync.js +282 -0
- package/dist/engine/compute-plan.d.ts +8 -1
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +303 -12
- package/dist/engine/engine.types.d.ts +61 -0
- package/dist/engine/engine.types.d.ts.map +1 -1
- package/dist/engine/execute-plan.d.ts +22 -2
- package/dist/engine/execute-plan.d.ts.map +1 -1
- package/dist/engine/execute-plan.js +459 -27
- package/dist/engine/provider-path-safety.d.ts +1 -0
- package/dist/engine/provider-path-safety.d.ts.map +1 -1
- package/dist/engine/provider-path-safety.js +15 -0
- package/dist/engine/scanner.d.ts +2 -2
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +21 -4
- package/dist/fs/io.d.ts +67 -0
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +536 -3
- package/dist/manifest/index.d.ts +2 -2
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/manager.d.ts +2 -2
- package/dist/manifest/manager.d.ts.map +1 -1
- package/dist/manifest/manager.js +20 -3
- package/dist/manifest/manifest.types.d.ts +704 -13
- package/dist/manifest/manifest.types.d.ts.map +1 -1
- package/dist/manifest/manifest.types.js +131 -15
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +41 -10
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +41 -10
- package/dist/providers/identity/absolute-paths.d.ts +68 -0
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
- package/dist/providers/identity/absolute-paths.js +144 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
- package/dist/providers/identity/claude-runtime-observation.js +229 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
- package/dist/providers/identity/codex-runtime-observation.js +255 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/generic-dispatch-record.js +539 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/oat-dispatch-record.js +691 -0
- package/dist/providers/identity/runtime-observation.d.ts +100 -0
- package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/runtime-observation.js +169 -0
- package/dist/providers/shared/index.d.ts +2 -0
- package/dist/providers/shared/index.d.ts.map +1 -1
- package/dist/providers/shared/index.js +1 -0
- package/dist/providers/shared/materialization-extension.d.ts +12 -0
- package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
- package/dist/providers/shared/registry.d.ts +69 -0
- package/dist/providers/shared/registry.d.ts.map +1 -0
- package/dist/providers/shared/registry.js +252 -0
- package/dist/providers/shared/restart-adviser.d.ts +26 -0
- package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
- package/dist/providers/shared/restart-adviser.js +96 -0
- package/dist/shared/types.d.ts +4 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { publishContainedJsonRevision, redactedFsError, withContainedWriterLock, } from '../../../fs/io.js';
|
|
4
|
+
import { assertJournalIdentityHasNoAbsolutePath, redactAbsolutePaths, } from '../../../providers/identity/absolute-paths.js';
|
|
5
|
+
import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
|
|
6
|
+
import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
|
|
7
|
+
import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
8
|
+
import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, } from '../../../providers/identity/oat-dispatch-record.js';
|
|
9
|
+
import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
|
|
10
|
+
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
11
|
+
const DISPATCH_LOCK_NAME = '.dispatch-lock';
|
|
12
|
+
/**
|
|
13
|
+
* The journal is append-only. Revision 1 keeps the plain `<request-id>.json`
|
|
14
|
+
* name; every later revision is published create-only under
|
|
15
|
+
* `<request-id>@<NNNN>.json`. Publishing revision N+1 exclusively is the
|
|
16
|
+
* compare-and-swap: a caller that lost the race observes `EEXIST` instead of
|
|
17
|
+
* replacing the winner, and no publication can ever clobber an existing file.
|
|
18
|
+
* `@` is outside the request-ID character class, so the split is unambiguous.
|
|
19
|
+
*/
|
|
20
|
+
const REVISION_SEPARATOR = '@';
|
|
21
|
+
const REVISION_SUFFIX_PATTERN = /^(.*)@(\d{4})$/;
|
|
22
|
+
const MAX_REVISION = 9999;
|
|
23
|
+
function revisionFileName(requestId, revision) {
|
|
24
|
+
return revision === 1
|
|
25
|
+
? `${requestId}.json`
|
|
26
|
+
: `${requestId}${REVISION_SEPARATOR}${String(revision).padStart(4, '0')}.json`;
|
|
27
|
+
}
|
|
28
|
+
function parseJournalEntry(fileName) {
|
|
29
|
+
if (!fileName.endsWith('.json'))
|
|
30
|
+
return null;
|
|
31
|
+
const stem = fileName.slice(0, -'.json'.length);
|
|
32
|
+
const suffixed = REVISION_SUFFIX_PATTERN.exec(stem);
|
|
33
|
+
const requestId = suffixed?.[1] ?? stem;
|
|
34
|
+
const revision = suffixed ? Number(suffixed[2]) : 1;
|
|
35
|
+
if (!REQUEST_ID_PATTERN.test(requestId) || revision < 1)
|
|
36
|
+
return null;
|
|
37
|
+
return { requestId, revision, fileName };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hard redaction boundary. Producers redact their own messages, but this is the
|
|
41
|
+
* last stop before a message reaches `--json`, so a future call site that
|
|
42
|
+
* forgets cannot regress NFR1. Known roots become stable labels; anything else
|
|
43
|
+
* that still looks like an absolute path is scrubbed.
|
|
44
|
+
*
|
|
45
|
+
* The detector is shared with the record sanitizer. Two independent notions of
|
|
46
|
+
* "looks like a path" is how a path could be scrubbed from an error message and
|
|
47
|
+
* written verbatim into the durable record by the same command.
|
|
48
|
+
*/
|
|
49
|
+
export function redactDispatchMessage(message, roots = {}) {
|
|
50
|
+
let redacted = message;
|
|
51
|
+
const labelled = [
|
|
52
|
+
['<project>', roots.project],
|
|
53
|
+
['<repo>', roots.repo],
|
|
54
|
+
['<home>', roots.home],
|
|
55
|
+
];
|
|
56
|
+
// Longest root first so a nested project path is not masked by its repo.
|
|
57
|
+
for (const [label, root] of [...labelled].sort(([, left], [, right]) => (right?.length ?? 0) - (left?.length ?? 0))) {
|
|
58
|
+
if (root)
|
|
59
|
+
redacted = redacted.split(root).join(label);
|
|
60
|
+
}
|
|
61
|
+
return redactAbsolutePaths(redacted);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Source strings that state provenance. A parser source is only ever written by
|
|
65
|
+
* the projection path that actually parsed provider output; a caller-supplied
|
|
66
|
+
* observation is always recorded as caller-asserted.
|
|
67
|
+
*/
|
|
68
|
+
const OBSERVATION_SOURCE_BY_PROVIDER = {
|
|
69
|
+
codex: CODEX_OBSERVATION_SOURCE,
|
|
70
|
+
claude: CLAUDE_OBSERVATION_SOURCE,
|
|
71
|
+
};
|
|
72
|
+
const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
|
|
73
|
+
function isRecord(value) {
|
|
74
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a post-launch observation event.
|
|
78
|
+
*
|
|
79
|
+
* A caller may submit either a finished `observation` or the sanitized provider
|
|
80
|
+
* `metadata` it was derived from. Submitting both is ambiguous and is refused
|
|
81
|
+
* rather than silently preferring one.
|
|
82
|
+
*
|
|
83
|
+
* Both forms are validated here, at the durable-write boundary, rather than
|
|
84
|
+
* being trusted from the producer. An earlier revision normalized only the
|
|
85
|
+
* metadata form, which left the finished-observation form able to assert its
|
|
86
|
+
* own `match`, name a provider the record does not use, and store values the
|
|
87
|
+
* metadata path would have refused. That is the per-producer-obligation class
|
|
88
|
+
* this boundary exists to prevent, so `match` is always derived and never read
|
|
89
|
+
* from the caller, and the provider is always bound to the record's own.
|
|
90
|
+
*
|
|
91
|
+
* Value shape is enforced once more in `runtimeObservationSchema`, so both
|
|
92
|
+
* forms converge on the same identifier rules without either path carrying its
|
|
93
|
+
* own copy of them.
|
|
94
|
+
*/
|
|
95
|
+
function resolveObservationEvent(record, event) {
|
|
96
|
+
if (event.kind !== 'runtime-observation')
|
|
97
|
+
return event;
|
|
98
|
+
if ('metadata' in event && 'observation' in event) {
|
|
99
|
+
throw new Error('A runtime observation event carries either observation or metadata, not both.');
|
|
100
|
+
}
|
|
101
|
+
if ('metadata' in event)
|
|
102
|
+
return event;
|
|
103
|
+
const observation = event.observation;
|
|
104
|
+
if (!isRecord(observation) || observation.status !== 'reported') {
|
|
105
|
+
return event;
|
|
106
|
+
}
|
|
107
|
+
if (observation.provider !== record.provider) {
|
|
108
|
+
throw new Error('A runtime observation must name the same provider as its dispatch record.');
|
|
109
|
+
}
|
|
110
|
+
if (!providerSupportsRuntimeObservation(record.provider)) {
|
|
111
|
+
throw new Error(`Provider ${record.provider} has no runtime observation capability, so it cannot report an observation.`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
...event,
|
|
115
|
+
observation: parseRuntimeObservation({
|
|
116
|
+
...observation,
|
|
117
|
+
// Provenance is derived, never accepted: a hand-authored observation
|
|
118
|
+
// cannot borrow a parser's source string and pass as a genuine parse.
|
|
119
|
+
source: CALLER_ASSERTED_OBSERVATION_SOURCE,
|
|
120
|
+
match: compareObservedRuntimeMetadata(observation, configuredInvocationForObservation(record)),
|
|
121
|
+
comparedAxes: comparedObservationAxes(observation, configuredInvocationForObservation(record)),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function parseDispatchRecordInput(value) {
|
|
126
|
+
if (!isRecord(value)) {
|
|
127
|
+
throw new Error('Dispatch record input must be a JSON object.');
|
|
128
|
+
}
|
|
129
|
+
const keys = Object.keys(value).sort();
|
|
130
|
+
if (keys.length !== 2 || keys[0] !== 'event' || keys[1] !== 'record') {
|
|
131
|
+
throw new Error('Dispatch record input accepts only record and event.');
|
|
132
|
+
}
|
|
133
|
+
if (!isRecord(value.event)) {
|
|
134
|
+
throw new Error('Dispatch record event must be a JSON object.');
|
|
135
|
+
}
|
|
136
|
+
// Raw provider metadata is the one input a caller may legitimately supply
|
|
137
|
+
// unmodified, so it is projected through the owning parser's allowlist and
|
|
138
|
+
// asserted as the projection. Everything else is asserted exactly as given.
|
|
139
|
+
// Asserting the raw entries instead would refuse real rollouts outright —
|
|
140
|
+
// a Codex `session_id` alone classifies as sensitive — which is what pushed
|
|
141
|
+
// sanitization onto callers and invited a hand-rolled stripper.
|
|
142
|
+
const rawMetadata = isRecord(value.event.metadata)
|
|
143
|
+
? value.event.metadata
|
|
144
|
+
: null;
|
|
145
|
+
assertNoSensitiveDispatchContent(rawMetadata === null
|
|
146
|
+
? value
|
|
147
|
+
: {
|
|
148
|
+
...value,
|
|
149
|
+
event: {
|
|
150
|
+
...value.event,
|
|
151
|
+
metadata: { ...rawMetadata, entries: null },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const record = parseGenericDispatchRecord(value.record);
|
|
155
|
+
if (rawMetadata !== null) {
|
|
156
|
+
if ('observation' in value.event) {
|
|
157
|
+
throw new Error('A runtime observation event carries either observation or metadata, not both.');
|
|
158
|
+
}
|
|
159
|
+
const envelope = parseRuntimeObservationEnvelope(rawMetadata);
|
|
160
|
+
const projected = envelope.provider === record.provider
|
|
161
|
+
? projectRuntimeObservation(envelope.provider, envelope.entries)
|
|
162
|
+
: {
|
|
163
|
+
facts: null,
|
|
164
|
+
reason: `observation metadata names provider ${envelope.provider}, but the record names ${record.provider}`,
|
|
165
|
+
};
|
|
166
|
+
const facts = projected.facts;
|
|
167
|
+
assertNoSensitiveDispatchContent(facts, '<observation-metadata>');
|
|
168
|
+
const { metadata: _metadata, ...rest } = value.event;
|
|
169
|
+
// Built here from parsed provider output, so it carries a parser source and
|
|
170
|
+
// must not be re-processed as if a caller had supplied it.
|
|
171
|
+
const observation = observationFromFacts({
|
|
172
|
+
record,
|
|
173
|
+
provider: envelope.provider,
|
|
174
|
+
source: OBSERVATION_SOURCE_BY_PROVIDER[envelope.provider] ?? 'unknown',
|
|
175
|
+
observedAt: envelope.observedAt,
|
|
176
|
+
facts,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
record,
|
|
180
|
+
event: { ...rest, observation },
|
|
181
|
+
observationReason: observation.status === 'reported'
|
|
182
|
+
? null
|
|
183
|
+
: // Facts can be produced and still be declined downstream, when the
|
|
184
|
+
// session declares a different request than this record does.
|
|
185
|
+
(projected.reason ??
|
|
186
|
+
'observation metadata correlates to a different request'),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
record,
|
|
191
|
+
event: resolveObservationEvent(record, value.event),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function runtimeIdentityFor(record, reason = null) {
|
|
195
|
+
const observation = record.oat.runtimeObservation;
|
|
196
|
+
return {
|
|
197
|
+
configured: {
|
|
198
|
+
roleName: record.role_name,
|
|
199
|
+
roleSelector: record.role_selector,
|
|
200
|
+
model: record.model_selector,
|
|
201
|
+
effort: record.effort_selector,
|
|
202
|
+
serviceTier: record.service_tier_selector ?? null,
|
|
203
|
+
},
|
|
204
|
+
observed: observation.status === 'reported'
|
|
205
|
+
? {
|
|
206
|
+
provider: observation.provider,
|
|
207
|
+
source: observation.source,
|
|
208
|
+
observedAt: observation.observedAt,
|
|
209
|
+
childLineage: observation.childLineage ?? null,
|
|
210
|
+
role: observation.role ?? null,
|
|
211
|
+
model: observation.model ?? null,
|
|
212
|
+
effort: observation.effort ?? null,
|
|
213
|
+
serviceTier: observation.serviceTier ?? null,
|
|
214
|
+
}
|
|
215
|
+
: null,
|
|
216
|
+
match: observation.status === 'reported' ? observation.match : null,
|
|
217
|
+
comparedAxes: observation.status === 'reported' ? (observation.comparedAxes ?? []) : [],
|
|
218
|
+
reason: observation.status === 'reported' ? null : reason,
|
|
219
|
+
status: observation.status,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function genericPart(record) {
|
|
223
|
+
const { oat: _oat, ...generic } = record;
|
|
224
|
+
return parseGenericDispatchRecord(generic);
|
|
225
|
+
}
|
|
226
|
+
function sameGenericRecord(left, right) {
|
|
227
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
228
|
+
}
|
|
229
|
+
function isMissingError(error) {
|
|
230
|
+
return (typeof error === 'object' &&
|
|
231
|
+
error !== null &&
|
|
232
|
+
'code' in error &&
|
|
233
|
+
error.code === 'ENOENT');
|
|
234
|
+
}
|
|
235
|
+
async function readPersistedRecord(path, reportedPath) {
|
|
236
|
+
let content;
|
|
237
|
+
try {
|
|
238
|
+
content = await readFile(path, 'utf8');
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (isMissingError(error)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
// A damaged or unreadable journal must not put an absolute project path
|
|
245
|
+
// into durable output; NFR1 treats that as a P0 disclosure.
|
|
246
|
+
throw redactedFsError(error, 'read', reportedPath);
|
|
247
|
+
}
|
|
248
|
+
return parsePersistedOatDispatchRecord(JSON.parse(content));
|
|
249
|
+
}
|
|
250
|
+
/** Highest published revision per request ID. */
|
|
251
|
+
async function readLatestRevisions(dispatchDir) {
|
|
252
|
+
let names;
|
|
253
|
+
try {
|
|
254
|
+
names = await readdir(dispatchDir);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (isMissingError(error)) {
|
|
258
|
+
return new Map();
|
|
259
|
+
}
|
|
260
|
+
throw redactedFsError(error, 'scan', 'dispatch/');
|
|
261
|
+
}
|
|
262
|
+
const latest = new Map();
|
|
263
|
+
for (const name of names.sort()) {
|
|
264
|
+
const entry = parseJournalEntry(name);
|
|
265
|
+
if (!entry)
|
|
266
|
+
continue;
|
|
267
|
+
const known = latest.get(entry.requestId);
|
|
268
|
+
if (!known || entry.revision > known.revision) {
|
|
269
|
+
latest.set(entry.requestId, entry);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return latest;
|
|
273
|
+
}
|
|
274
|
+
function fallbackTriggerRequestId(event) {
|
|
275
|
+
if (event.kind !== 'fallback-link')
|
|
276
|
+
return null;
|
|
277
|
+
const requestId = event.evidence?.triggerRequestId;
|
|
278
|
+
if (typeof requestId !== 'string' || !REQUEST_ID_PATTERN.test(requestId)) {
|
|
279
|
+
throw new Error('Fallback trigger request ID is invalid.');
|
|
280
|
+
}
|
|
281
|
+
return requestId;
|
|
282
|
+
}
|
|
283
|
+
function isAlreadyPublished(error) {
|
|
284
|
+
return (typeof error === 'object' &&
|
|
285
|
+
error !== null &&
|
|
286
|
+
'code' in error &&
|
|
287
|
+
error.code === 'EEXIST');
|
|
288
|
+
}
|
|
289
|
+
async function publishRevision(projectPath, dispatchDir, requestId, revision, record) {
|
|
290
|
+
if (revision > MAX_REVISION) {
|
|
291
|
+
throw new Error(`Dispatch journal for ${requestId} reached its ${MAX_REVISION}-revision limit.`);
|
|
292
|
+
}
|
|
293
|
+
const fileName = revisionFileName(requestId, revision);
|
|
294
|
+
// Last check before bytes reach the journal. The sanitizer runs at parse, but
|
|
295
|
+
// the journal is committed to a shared repository, so the guarantee is
|
|
296
|
+
// enforced on the exact value being written rather than trusted upstream.
|
|
297
|
+
// Guarantees identity and control fields only; prose redaction is
|
|
298
|
+
// best-effort by design. See absolute-paths.ts.
|
|
299
|
+
assertJournalIdentityHasNoAbsolutePath(identityFieldsOf(genericPart(record)));
|
|
300
|
+
assertBoundedDispatchRecordSize(record);
|
|
301
|
+
try {
|
|
302
|
+
await publishContainedJsonRevision(join(dispatchDir, fileName), record, projectPath);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (isAlreadyPublished(error)) {
|
|
306
|
+
throw new Error('The dispatch journal advanced to a newer revision while this write was prepared; the concurrent update was preserved and this stale write was refused.', { cause: error });
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
return `dispatch/${fileName}`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse once, here. An earlier revision parsed in the command layer and again
|
|
314
|
+
* here; because the metadata path resolves to a finished observation, the second
|
|
315
|
+
* parse saw its own output as caller-supplied and overwrote the parser source
|
|
316
|
+
* with `caller-asserted`. Parsing is not idempotent by design — provenance
|
|
317
|
+
* depends on which input form arrived — so it must happen exactly once.
|
|
318
|
+
*/
|
|
319
|
+
export async function recordProjectDispatch(input) {
|
|
320
|
+
const parsedInput = parseDispatchRecordInput(input.input);
|
|
321
|
+
if (input.projectPath === null) {
|
|
322
|
+
const record = augmentDispatchRecord(parsedInput);
|
|
323
|
+
return {
|
|
324
|
+
status: 'validated-only',
|
|
325
|
+
path: null,
|
|
326
|
+
created: false,
|
|
327
|
+
record,
|
|
328
|
+
runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const projectPath = input.projectPath;
|
|
332
|
+
const dispatchDir = join(projectPath, 'dispatch');
|
|
333
|
+
const requestId = parsedInput.record.request_id;
|
|
334
|
+
const latest = await readLatestRevisions(dispatchDir);
|
|
335
|
+
const existingEntry = latest.get(requestId) ?? null;
|
|
336
|
+
const existing = existingEntry
|
|
337
|
+
? await readPersistedRecord(join(dispatchDir, existingEntry.fileName), `dispatch/${existingEntry.fileName}`)
|
|
338
|
+
: null;
|
|
339
|
+
if (existing &&
|
|
340
|
+
!sameGenericRecord(genericPart(existing), parsedInput.record)) {
|
|
341
|
+
throw new Error('Existing generic fields are immutable and cannot be redefined.');
|
|
342
|
+
}
|
|
343
|
+
const relatedRecords = [];
|
|
344
|
+
for (const [otherId, entry] of [...latest].sort(([left], [right]) => left < right ? -1 : 1)) {
|
|
345
|
+
if (otherId === requestId)
|
|
346
|
+
continue;
|
|
347
|
+
const related = await readPersistedRecord(join(dispatchDir, entry.fileName), `dispatch/${entry.fileName}`);
|
|
348
|
+
if (related)
|
|
349
|
+
relatedRecords.push(related);
|
|
350
|
+
}
|
|
351
|
+
const triggerRequestId = fallbackTriggerRequestId(parsedInput.event);
|
|
352
|
+
const triggerEntry = triggerRequestId
|
|
353
|
+
? (latest.get(triggerRequestId) ?? null)
|
|
354
|
+
: null;
|
|
355
|
+
const trigger = triggerEntry
|
|
356
|
+
? await readPersistedRecord(join(dispatchDir, triggerEntry.fileName), `dispatch/${triggerEntry.fileName}`)
|
|
357
|
+
: null;
|
|
358
|
+
if (triggerRequestId !== null && trigger === null) {
|
|
359
|
+
throw new Error('Fallback requires the rejected trigger record.');
|
|
360
|
+
}
|
|
361
|
+
await input.raceBarriers?.afterRead?.();
|
|
362
|
+
// One shared, identity-bound transition. The lock serializes the trigger
|
|
363
|
+
// claim and the fallback publication; the exclusive revision name is the
|
|
364
|
+
// compare-and-swap, so a stale concurrent writer loses without any write
|
|
365
|
+
// being able to replace the winner's evidence.
|
|
366
|
+
return withContainedWriterLock(join(projectPath, DISPATCH_LOCK_NAME), projectPath, async () => {
|
|
367
|
+
let triggerRecord = trigger ?? undefined;
|
|
368
|
+
if (trigger && triggerEntry && triggerRequestId) {
|
|
369
|
+
const claimed = augmentDispatchRecord({
|
|
370
|
+
record: trigger,
|
|
371
|
+
event: {
|
|
372
|
+
kind: 'fallback-claim',
|
|
373
|
+
requestId: triggerRequestId,
|
|
374
|
+
source: 'provider-wrapper',
|
|
375
|
+
claim: {
|
|
376
|
+
fallbackRequestId: requestId,
|
|
377
|
+
claimedAt: new Date().toISOString(),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
if (JSON.stringify(claimed) !== JSON.stringify(trigger)) {
|
|
382
|
+
await publishRevision(projectPath, dispatchDir, triggerRequestId, triggerEntry.revision + 1, claimed);
|
|
383
|
+
}
|
|
384
|
+
triggerRecord = claimed;
|
|
385
|
+
}
|
|
386
|
+
const record = augmentDispatchRecord({
|
|
387
|
+
record: existing ?? parsedInput.record,
|
|
388
|
+
event: parsedInput.event,
|
|
389
|
+
...(triggerRecord ? { triggerRecord } : {}),
|
|
390
|
+
relatedRecords,
|
|
391
|
+
});
|
|
392
|
+
const revision = (existingEntry?.revision ?? 0) + 1;
|
|
393
|
+
const path = await publishRevision(projectPath, dispatchDir, requestId, revision, record);
|
|
394
|
+
return {
|
|
395
|
+
status: 'persisted',
|
|
396
|
+
path,
|
|
397
|
+
created: revision === 1,
|
|
398
|
+
record,
|
|
399
|
+
runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAuB9C"}
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { createProjectArchiveCommand } from './archive/index.js';
|
|
3
3
|
import { createProjectCompleteDiscoveryCommand } from './complete-discovery/index.js';
|
|
4
4
|
import { createProjectCompleteStateCommand } from './complete-state/index.js';
|
|
5
|
+
import { createProjectDispatchCommand } from './dispatch/index.js';
|
|
5
6
|
import { createProjectDispatchCeilingCommand } from './dispatch-ceiling/index.js';
|
|
6
7
|
import { createProjectLinksCommand } from './links/index.js';
|
|
7
8
|
import { createProjectListCommand } from './list.js';
|
|
@@ -24,6 +25,7 @@ export function createProjectCommand() {
|
|
|
24
25
|
.addCommand(createProjectArchiveCommand())
|
|
25
26
|
.addCommand(createProjectCompleteDiscoveryCommand())
|
|
26
27
|
.addCommand(createProjectCompleteStateCommand())
|
|
28
|
+
.addCommand(createProjectDispatchCommand())
|
|
27
29
|
.addCommand(createProjectDispatchCeilingCommand())
|
|
28
30
|
.addCommand(createProjectListCommand())
|
|
29
31
|
.addCommand(createProjectLinksCommand())
|
|
@@ -4,6 +4,7 @@ import { type LinksInput } from './render.js';
|
|
|
4
4
|
export interface ComputeLinksOptions {
|
|
5
5
|
durableSummaryPath?: string;
|
|
6
6
|
now: Date;
|
|
7
|
+
ref?: string;
|
|
7
8
|
}
|
|
8
9
|
export declare function computeLinksInput(target: SyncTarget, git: GitRunner, options: ComputeLinksOptions): Promise<LinksInput>;
|
|
9
10
|
//# sourceMappingURL=compute.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compute.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/compute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"compute.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/compute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CA8BrB"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { LINKABLE_ARTIFACTS, } from './render.js';
|
|
2
2
|
export async function computeLinksInput(target, git, options) {
|
|
3
|
-
|
|
3
|
+
const selectedRef = options.ref ?? target.ref;
|
|
4
|
+
await git.run(['fetch', target.remote, `+${selectedRef}:${selectedRef}`], {
|
|
4
5
|
cwd: target.repoRoot,
|
|
5
6
|
});
|
|
6
7
|
const [sha, tree, origin] = await Promise.all([
|
|
7
|
-
git.run(['rev-parse',
|
|
8
|
-
git.run(['ls-tree', '--name-only',
|
|
8
|
+
git.run(['rev-parse', selectedRef], { cwd: target.repoRoot }),
|
|
9
|
+
git.run(['ls-tree', '--name-only', selectedRef], { cwd: target.repoRoot }),
|
|
9
10
|
git.run(['remote', 'get-url', target.remote], { cwd: target.repoRoot }),
|
|
10
11
|
]);
|
|
11
12
|
const treeNames = new Set(tree.stdout
|
|
@@ -16,7 +17,7 @@ export async function computeLinksInput(target, git, options) {
|
|
|
16
17
|
return {
|
|
17
18
|
slug: target.slug,
|
|
18
19
|
sha: sha.stdout,
|
|
19
|
-
ref:
|
|
20
|
+
ref: selectedRef,
|
|
20
21
|
originUrl: origin.stdout,
|
|
21
22
|
present,
|
|
22
23
|
...(options.durableSummaryPath
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
|
|
2
2
|
import { type GitRunner } from '../../project/sync/git.js';
|
|
3
|
+
import { type SyncTarget } from '../../project/sync/ref-sync.js';
|
|
3
4
|
import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
|
|
5
|
+
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
4
6
|
import { Command } from 'commander';
|
|
5
7
|
import { computeLinksInput } from './compute.js';
|
|
6
8
|
interface ProjectLinksDependencies {
|
|
7
9
|
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
8
10
|
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
11
|
+
resolveProjectsRoot: typeof resolveProjectsRoot;
|
|
9
12
|
resolveSyncedTarget: typeof resolveSyncedTarget;
|
|
13
|
+
resolveLinksTarget: typeof resolveLinksTarget;
|
|
10
14
|
computeLinksInput: typeof computeLinksInput;
|
|
11
15
|
gitRunner: GitRunner;
|
|
12
16
|
processEnv: NodeJS.ProcessEnv;
|
|
13
17
|
now: () => Date;
|
|
14
18
|
}
|
|
19
|
+
interface ResolvedLinksTarget {
|
|
20
|
+
target: SyncTarget;
|
|
21
|
+
ref: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveLinksTarget(repoRoot: string, projectsRoot: string, pathOrSlug: string | undefined, env: NodeJS.ProcessEnv, git: GitRunner, resolveTarget: typeof resolveSyncedTarget): Promise<ResolvedLinksTarget>;
|
|
15
24
|
export declare function createProjectLinksCommand(overrides?: Partial<ProjectLinksDependencies>): Command;
|
|
16
25
|
export {};
|
|
17
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,mBAAmB,EACpB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AASjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAQ9C,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAmCD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAwBD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,EAAE,SAAS,EACd,aAAa,EAAE,OAAO,mBAAmB,GACxC,OAAO,CAAC,mBAAmB,CAAC,CAgE9B;AAoDD,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAsBT"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
2
|
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
3
3
|
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildSyncTarget, classifyRemoteRefLookup, } from '../../project/sync/ref-sync.js';
|
|
5
|
+
import { probeSyncedTerminalRefs, resolveSyncedTarget, } from '../../project/sync/resolve-target.js';
|
|
6
|
+
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
7
|
+
import { completedSyncedRefName, resolveProjectScope, resolveScopeRoot, } from '../../shared/project-scope.js';
|
|
5
8
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
6
9
|
import { CliError } from '../../../errors/cli-error.js';
|
|
7
10
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
@@ -11,7 +14,9 @@ import { renderLinksBlock } from './render.js';
|
|
|
11
14
|
const DEFAULT_DEPENDENCIES = {
|
|
12
15
|
buildCommandContext,
|
|
13
16
|
resolveProjectRoot,
|
|
17
|
+
resolveProjectsRoot,
|
|
14
18
|
resolveSyncedTarget,
|
|
19
|
+
resolveLinksTarget,
|
|
15
20
|
computeLinksInput,
|
|
16
21
|
gitRunner: defaultGitRunner,
|
|
17
22
|
processEnv: process.env,
|
|
@@ -30,13 +35,77 @@ function normalizeDurableSummaryPath(repoRoot, durableSummaryPath) {
|
|
|
30
35
|
}
|
|
31
36
|
return repositoryRelative.split(sep).join('/');
|
|
32
37
|
}
|
|
38
|
+
function linksSlug(repoRoot, projectsRoot, pathOrSlug) {
|
|
39
|
+
if (!pathOrSlug.includes('/') && !pathOrSlug.includes('\\')) {
|
|
40
|
+
return pathOrSlug;
|
|
41
|
+
}
|
|
42
|
+
const absolute = isAbsolute(pathOrSlug)
|
|
43
|
+
? resolve(pathOrSlug)
|
|
44
|
+
: resolve(repoRoot, pathOrSlug);
|
|
45
|
+
const sharedRoot = resolveScopeRoot(repoRoot, projectsRoot, 'shared');
|
|
46
|
+
if (resolveProjectScope(absolute, sharedRoot, repoRoot) !== 'synced') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const child = relative(resolveScopeRoot(repoRoot, projectsRoot, 'synced'), absolute);
|
|
50
|
+
return child && !child.includes(sep) ? child : null;
|
|
51
|
+
}
|
|
52
|
+
export async function resolveLinksTarget(repoRoot, projectsRoot, pathOrSlug, env, git, resolveTarget) {
|
|
53
|
+
let resolved = null;
|
|
54
|
+
let resolutionError;
|
|
55
|
+
try {
|
|
56
|
+
resolved = await resolveTarget({ repoRoot, env }, pathOrSlug, { gitRunner: git }, { allowMissingCheckout: true });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
resolutionError = error;
|
|
60
|
+
if (!(error instanceof CliError) ||
|
|
61
|
+
error.exitCode !== 1 ||
|
|
62
|
+
!error.message.startsWith('No synced project named ')) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const slug = pathOrSlug
|
|
67
|
+
? linksSlug(repoRoot, projectsRoot, pathOrSlug)
|
|
68
|
+
: resolved?.slug;
|
|
69
|
+
if (!slug) {
|
|
70
|
+
if (resolutionError)
|
|
71
|
+
throw resolutionError;
|
|
72
|
+
throw new CliError('No synced project is available for link rendering.', 1);
|
|
73
|
+
}
|
|
74
|
+
const target = resolved ?? buildSyncTarget(repoRoot, projectsRoot, slug);
|
|
75
|
+
const completedRef = completedSyncedRefName(slug);
|
|
76
|
+
const lookup = await git.run(['ls-remote', '--exit-code', target.remote, completedRef], { cwd: repoRoot, allowFailure: true });
|
|
77
|
+
if (classifyRemoteRefLookup(lookup, target.remote, completedRef) === 'absent') {
|
|
78
|
+
if (resolutionError)
|
|
79
|
+
throw resolutionError;
|
|
80
|
+
return { target, ref: target.ref };
|
|
81
|
+
}
|
|
82
|
+
const rows = lookup.stdout.split('\n').filter(Boolean);
|
|
83
|
+
const [sha, ref] = rows[0]?.trim().split(/\s+/) ?? [];
|
|
84
|
+
if (rows.length !== 1 ||
|
|
85
|
+
ref !== completedRef ||
|
|
86
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(sha ?? '')) {
|
|
87
|
+
throw new CliError(`Unable to verify terminal links for ${slug}: origin returned a malformed completed-ref advertisement.`, 2);
|
|
88
|
+
}
|
|
89
|
+
const probe = await probeSyncedTerminalRefs(target, sha, git);
|
|
90
|
+
if (probe.state === 'wrong-sha') {
|
|
91
|
+
throw new CliError(`Cannot refresh links for ${slug}: active ${probe.activeRef} is ${probe.activeSha ?? 'absent'} while completed ${probe.completedRef} is ${probe.completedSha ?? 'absent'}. Repair the terminal ref mismatch first.`, 1);
|
|
92
|
+
}
|
|
93
|
+
if (probe.state === 'completed-only' || probe.state === 'both') {
|
|
94
|
+
return { target, ref: completedRef };
|
|
95
|
+
}
|
|
96
|
+
if (resolutionError)
|
|
97
|
+
throw resolutionError;
|
|
98
|
+
return { target, ref: target.ref };
|
|
99
|
+
}
|
|
33
100
|
async function runLinks(context, pathOrSlug, options, dependencies) {
|
|
34
101
|
try {
|
|
35
102
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
36
|
-
const
|
|
103
|
+
const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
|
|
104
|
+
const { target, ref } = await dependencies.resolveLinksTarget(repoRoot, projectsRoot, pathOrSlug, dependencies.processEnv, dependencies.gitRunner, dependencies.resolveSyncedTarget);
|
|
37
105
|
const input = await dependencies.computeLinksInput(target, dependencies.gitRunner, {
|
|
38
106
|
durableSummaryPath: normalizeDurableSummaryPath(repoRoot, options.durableSummary),
|
|
39
107
|
now: dependencies.now(),
|
|
108
|
+
ref,
|
|
40
109
|
});
|
|
41
110
|
const markdown = renderLinksBlock(input);
|
|
42
111
|
if (context.json || options.format === 'json') {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CommandContext, type GlobalOptions } from '../../app/command-context.js';
|
|
2
2
|
import { type GitRunner } from '../project/sync/git.js';
|
|
3
|
-
import { listSyncedRecords } from '../project/sync/record.js';
|
|
4
|
-
import {
|
|
3
|
+
import { listSyncedRecords, type SyncedProjectRecord } from '../project/sync/record.js';
|
|
4
|
+
import { buildSyncTarget } from '../project/sync/ref-sync.js';
|
|
5
|
+
import { probeSyncedTerminalRefs, type SyncedTerminalRefProbe } from '../project/sync/resolve-target.js';
|
|
6
|
+
import { type ProjectListRow, type ProjectSummary } from '@open-agent-toolkit/control-plane';
|
|
5
7
|
import { Command } from 'commander';
|
|
6
8
|
interface ProjectListDependencies {
|
|
7
9
|
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
@@ -9,6 +11,8 @@ interface ProjectListDependencies {
|
|
|
9
11
|
resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
|
|
10
12
|
listProjects: (projectsRoot: string) => Promise<ProjectSummary[]>;
|
|
11
13
|
listSyncedRecords: typeof listSyncedRecords;
|
|
14
|
+
probeSyncedTerminalRefs: typeof probeSyncedTerminalRefs;
|
|
15
|
+
probeAuthoritativeCompletedRef: typeof probeAuthoritativeCompletedRef;
|
|
12
16
|
directoryExists: (path: string) => Promise<boolean>;
|
|
13
17
|
readProjectMetadata: (projectPath: string) => Promise<ProjectListMetadata>;
|
|
14
18
|
processEnv: NodeJS.ProcessEnv;
|
|
@@ -19,6 +23,10 @@ interface ProjectListMetadata {
|
|
|
19
23
|
phase: string;
|
|
20
24
|
phaseStatus: string;
|
|
21
25
|
}
|
|
26
|
+
export declare function classifyLegacySyncedRecord(record: SyncedProjectRecord, path: string, probe?: SyncedTerminalRefProbe): Extract<ProjectListRow, {
|
|
27
|
+
kind: 'recorded-absent' | 'recorded-terminal' | 'terminal-invalid';
|
|
28
|
+
}>;
|
|
29
|
+
export declare function probeAuthoritativeCompletedRef(target: ReturnType<typeof buildSyncTarget>, git: GitRunner): Promise<SyncedTerminalRefProbe | null>;
|
|
22
30
|
export declare function createProjectListCommand(overrides?: Partial<ProjectListDependencies>): Command;
|
|
23
31
|
export {};
|
|
24
32
|
//# sourceMappingURL=list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/project/list.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/project/list.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,iBAAiB,EACjB,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,eAAe,EAEhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,KAAK,sBAAsB,EAC5B,MAAM,uCAAuC,CAAC;AAY/C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAE5C,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAClE,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,8BAA8B,EAAE,OAAO,8BAA8B,CAAC;IACtE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC3E,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AA6KD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,sBAAsB,GAC7B,OAAO,CACR,cAAc,EACd;IAAE,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,CAAA;CAAE,CACvE,CA0FA;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,EAC1C,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwBxC;AA8TD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwBT"}
|