@open-agent-toolkit/cli 0.2.65 → 0.2.67
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/configuration.md +33 -2
- package/assets/docs/cli-utilities/index.md +3 -0
- package/assets/docs/cli-utilities/project-log.md +63 -4
- package/assets/docs/cli-utilities/remote-project-management.md +191 -0
- package/assets/docs/contributing/code.md +8 -4
- package/assets/docs/contributing/skills.md +29 -17
- package/assets/docs/docs-tooling/commands.md +3 -1
- package/assets/docs/provider-sync/manifest-and-drift.md +2 -18
- package/assets/docs/reference/cli-reference.md +12 -1
- package/assets/docs/reference/file-locations.md +22 -0
- package/assets/docs/reference/oat-directory-structure.md +30 -17
- package/assets/docs/reference/troubleshooting.md +44 -0
- package/assets/docs/workflows/projects/lifecycle.md +17 -0
- package/assets/docs/workflows/projects/picking-up-projects.md +18 -0
- package/assets/docs/workflows/skills/index.md +33 -0
- package/assets/docs/workflows/skills/repo-improve.md +7 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/create-agnostic-skill/SKILL.md +19 -37
- package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +58 -15
- package/assets/skills/create-agnostic-skill/references/skill-template.md +17 -15
- package/assets/skills/oat-agent-instructions-apply/SKILL.md +1 -1
- package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/glob-scoped-rule.md +1 -4
- package/assets/skills/oat-doctor/SKILL.md +3 -6
- package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
- package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-complete/SKILL.md +158 -30
- package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +26 -7
- package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +183 -0
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-retro/SKILL.md +1 -1
- package/assets/skills/oat-project-retro/references/apply-procedure.md +11 -0
- package/assets/skills/oat-project-review-provide/SKILL.md +4 -4
- package/assets/skills/oat-project-summary/SKILL.md +24 -2
- package/assets/skills/oat-repo-improve/SKILL.md +9 -2
- package/assets/skills/oat-repo-improve/references/plan-template.md +4 -6
- package/assets/skills/oat-repo-knowledge-index/SKILL.md +11 -9
- package/assets/skills/oat-wave-execute/SKILL.md +20 -6
- package/assets/skills/oat-wave-program/SKILL.md +9 -4
- package/dist/commands/backlog/new.d.ts +1 -0
- package/dist/commands/backlog/new.d.ts.map +1 -1
- package/dist/commands/backlog/new.js +3 -1
- package/dist/commands/config/index.d.ts +3 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +362 -36
- package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/index.js +68 -12
- package/dist/commands/docs/init/index.d.ts +11 -0
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +19 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +10 -2
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +42 -9
- package/dist/commands/internal/validate-skill-version-bumps.d.ts.map +1 -1
- package/dist/commands/internal/validate-skill-version-bumps.js +5 -5
- package/dist/commands/pjm/doctor.d.ts +2 -0
- package/dist/commands/pjm/doctor.d.ts.map +1 -1
- package/dist/commands/pjm/doctor.js +71 -1
- package/dist/commands/pjm/index.d.ts +2 -0
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +3 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
- package/dist/commands/pjm/remote/association.d.ts +27 -0
- package/dist/commands/pjm/remote/association.d.ts.map +1 -0
- package/dist/commands/pjm/remote/association.js +68 -0
- package/dist/commands/pjm/remote/authority.d.ts +210 -0
- package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
- package/dist/commands/pjm/remote/authority.js +341 -0
- package/dist/commands/pjm/remote/batch.d.ts +15 -0
- package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
- package/dist/commands/pjm/remote/batch.js +126 -0
- package/dist/commands/pjm/remote/closeout.d.ts +69 -0
- package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
- package/dist/commands/pjm/remote/closeout.js +208 -0
- package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
- package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
- package/dist/commands/pjm/remote/create-binding.js +126 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/credential-safety.js +21 -0
- package/dist/commands/pjm/remote/discussion.d.ts +49 -0
- package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
- package/dist/commands/pjm/remote/discussion.js +124 -0
- package/dist/commands/pjm/remote/doctor.d.ts +22 -0
- package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
- package/dist/commands/pjm/remote/doctor.js +380 -0
- package/dist/commands/pjm/remote/external-action.d.ts +208 -0
- package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
- package/dist/commands/pjm/remote/external-action.js +367 -0
- package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
- package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/host-execution.js +63 -0
- package/dist/commands/pjm/remote/index.d.ts +41 -0
- package/dist/commands/pjm/remote/index.d.ts.map +1 -0
- package/dist/commands/pjm/remote/index.js +280 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
- package/dist/commands/pjm/remote/jira-adf.js +169 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
- package/dist/commands/pjm/remote/lifecycle.js +224 -0
- package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
- package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
- package/dist/commands/pjm/remote/local-projection.js +95 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
- package/dist/commands/pjm/remote/managed-markdown.js +128 -0
- package/dist/commands/pjm/remote/migrate.d.ts +14 -0
- package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
- package/dist/commands/pjm/remote/migrate.js +32 -0
- package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
- package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
- package/dist/commands/pjm/remote/operation-state.js +143 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
- package/dist/commands/pjm/remote/output.d.ts +60 -0
- package/dist/commands/pjm/remote/output.d.ts.map +1 -0
- package/dist/commands/pjm/remote/output.js +48 -0
- package/dist/commands/pjm/remote/preview.d.ts +88 -0
- package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
- package/dist/commands/pjm/remote/preview.js +154 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider-conformance.js +37 -0
- package/dist/commands/pjm/remote/provider.d.ts +57 -0
- package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider.js +28 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
- package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
- package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github.js +1456 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/jira.js +1650 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/linear.js +1289 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
- package/dist/commands/pjm/remote/purpose-policy.js +118 -0
- package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
- package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
- package/dist/commands/pjm/remote/reconcile.js +101 -0
- package/dist/commands/pjm/remote/resolution.d.ts +122 -0
- package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/resolution.js +243 -0
- package/dist/commands/pjm/remote/schema.d.ts +9309 -0
- package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
- package/dist/commands/pjm/remote/schema.js +1503 -0
- package/dist/commands/pjm/remote/service.d.ts +20 -0
- package/dist/commands/pjm/remote/service.d.ts.map +1 -0
- package/dist/commands/pjm/remote/service.js +5121 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
- package/dist/commands/pjm/remote/shared-storage.js +110 -0
- package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
- package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
- package/dist/commands/pjm/remote/snapshot.js +140 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
- package/dist/commands/pjm/remote/storage-locator.js +62 -0
- package/dist/commands/pjm/remote/store.d.ts +85 -0
- package/dist/commands/pjm/remote/store.d.ts.map +1 -0
- package/dist/commands/pjm/remote/store.js +553 -0
- package/dist/commands/pjm/remote/verification.d.ts +40 -0
- package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
- package/dist/commands/pjm/remote/verification.js +100 -0
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +32 -18
- package/dist/commands/project/log/append.d.ts +48 -0
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +207 -30
- package/dist/commands/project/log/check.d.ts +87 -1
- package/dist/commands/project/log/check.d.ts.map +1 -1
- package/dist/commands/project/log/check.js +139 -5
- package/dist/commands/project/log/grammar.d.ts +165 -0
- package/dist/commands/project/log/grammar.d.ts.map +1 -1
- package/dist/commands/project/log/grammar.js +219 -1
- package/dist/commands/project/log/rollup.d.ts +16 -1
- package/dist/commands/project/log/rollup.d.ts.map +1 -1
- package/dist/commands/project/log/rollup.js +22 -2
- package/dist/commands/project/log/synthesize.d.ts +16 -0
- package/dist/commands/project/log/synthesize.d.ts.map +1 -1
- package/dist/commands/project/log/synthesize.js +85 -9
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +24 -2
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +6 -4
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +1 -0
- package/dist/config/dispatch-matrix.d.ts.map +1 -1
- package/dist/config/dispatch-matrix.js +13 -5
- package/dist/config/oat-config.d.ts +41 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +247 -9
- package/dist/config/own-keys.d.ts +22 -0
- package/dist/config/own-keys.d.ts.map +1 -0
- package/dist/config/own-keys.js +32 -0
- package/dist/config/resolve.d.ts +11 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +35 -11
- package/dist/config/sync-config.d.ts.map +1 -1
- package/dist/config/sync-config.js +16 -0
- package/dist/drift/detector.d.ts.map +1 -1
- package/dist/drift/detector.js +52 -1
- package/dist/drift/skill-view-diagnostic.d.ts.map +1 -1
- package/dist/drift/skill-view-diagnostic.js +2 -23
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +22 -72
- package/dist/engine/managed-copy-hash.d.ts +69 -0
- package/dist/engine/managed-copy-hash.d.ts.map +1 -0
- package/dist/engine/managed-copy-hash.js +175 -0
- package/dist/fs/assets.d.ts +11 -0
- package/dist/fs/assets.d.ts.map +1 -1
- package/dist/fs/assets.js +1 -1
- package/dist/manifest/hash.d.ts +110 -0
- package/dist/manifest/hash.d.ts.map +1 -1
- package/dist/manifest/hash.js +111 -13
- package/dist/providers/ceiling/registry.d.ts.map +1 -1
- package/dist/providers/ceiling/registry.js +6 -1
- package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
- package/dist/providers/identity/dispatch-report.js +4 -1
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +21 -0
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +305 -40
- package/package.json +2 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const FIELD_ORDER = [
|
|
3
|
+
'title',
|
|
4
|
+
'description',
|
|
5
|
+
'priority',
|
|
6
|
+
];
|
|
7
|
+
export function buildVerificationRequestDigest(input) {
|
|
8
|
+
const fieldMask = normalizeFieldMask(input.requested.fieldMask);
|
|
9
|
+
return hashCanonical({
|
|
10
|
+
operationId: input.operationId,
|
|
11
|
+
fieldMask,
|
|
12
|
+
expected: Object.fromEntries(fieldMask.map((field) => [field, input.requested.expected[field]])),
|
|
13
|
+
attemptedRevisionDigest: input.revisionEvidence.attemptedRevisionDigest,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function verifyRemotePostconditions(input) {
|
|
17
|
+
const fieldMask = normalizeFieldMask(input.requested.fieldMask);
|
|
18
|
+
for (const field of fieldMask) {
|
|
19
|
+
if (!Object.hasOwn(input.requested.expected, field)) {
|
|
20
|
+
throw new Error(`Requested field '${field}' has no expected value.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const requestDigest = buildVerificationRequestDigest(input);
|
|
24
|
+
if (input.priorVerification?.classification === 'verified' &&
|
|
25
|
+
input.priorVerification.requestDigest === requestDigest) {
|
|
26
|
+
return result('already-verified', 'durable-verification-match', requestDigest, [], false);
|
|
27
|
+
}
|
|
28
|
+
if (input.providerOutcome === 'rejected') {
|
|
29
|
+
return result('rejected', 'provider-rejected', requestDigest, unavailableFields(input, fieldMask), false);
|
|
30
|
+
}
|
|
31
|
+
if (input.providerOutcome === 'ambiguous') {
|
|
32
|
+
return result('uncertain', 'ambiguous-provider-outcome', requestDigest, unavailableFields(input, fieldMask), true);
|
|
33
|
+
}
|
|
34
|
+
if (input.revisionEvidence.readbackBaseRevisionDigest !==
|
|
35
|
+
input.revisionEvidence.attemptedRevisionDigest) {
|
|
36
|
+
return result('uncertain', 'revision-drift', requestDigest, unavailableFields(input, fieldMask), true);
|
|
37
|
+
}
|
|
38
|
+
if (!input.readback) {
|
|
39
|
+
return result('uncertain', 'missing-readback', requestDigest, unavailableFields(input, fieldMask), true);
|
|
40
|
+
}
|
|
41
|
+
const fields = fieldMask.map((field) => verifyField(field, input.requested.expected[field], input.readback));
|
|
42
|
+
const verifiedCount = fields.filter((field) => field.status === 'verified').length;
|
|
43
|
+
const unavailableCount = fields.filter((field) => field.status === 'unavailable').length;
|
|
44
|
+
if (verifiedCount === fields.length) {
|
|
45
|
+
return result('verified', 'postconditions-verified', requestDigest, fields, false);
|
|
46
|
+
}
|
|
47
|
+
if (verifiedCount > 0) {
|
|
48
|
+
return result('partial', 'partial-postconditions', requestDigest, fields, true);
|
|
49
|
+
}
|
|
50
|
+
if (unavailableCount > 0) {
|
|
51
|
+
return result('uncertain', 'incomplete-readback', requestDigest, fields, true);
|
|
52
|
+
}
|
|
53
|
+
return result('uncertain', 'postconditions-mismatched', requestDigest, fields, true);
|
|
54
|
+
}
|
|
55
|
+
function verifyField(field, expected, readback) {
|
|
56
|
+
const expectedHash = hashCanonical(expected);
|
|
57
|
+
if (readback.unavailableFields.includes(field) ||
|
|
58
|
+
!Object.hasOwn(readback.values, field)) {
|
|
59
|
+
return { field, expectedHash, observedHash: null, status: 'unavailable' };
|
|
60
|
+
}
|
|
61
|
+
const observed = readback.values[field];
|
|
62
|
+
return {
|
|
63
|
+
field,
|
|
64
|
+
expectedHash,
|
|
65
|
+
observedHash: hashCanonical(observed),
|
|
66
|
+
status: observed === expected ? 'verified' : 'mismatch',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function unavailableFields(input, fieldMask) {
|
|
70
|
+
return fieldMask.map((field) => ({
|
|
71
|
+
field,
|
|
72
|
+
expectedHash: hashCanonical(input.requested.expected[field]),
|
|
73
|
+
observedHash: null,
|
|
74
|
+
status: 'unavailable',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
function result(classification, reason, requestDigest, fields, requiresReconciliation) {
|
|
78
|
+
return {
|
|
79
|
+
classification,
|
|
80
|
+
reason,
|
|
81
|
+
requestDigest,
|
|
82
|
+
fields,
|
|
83
|
+
retryDisposition: requiresReconciliation
|
|
84
|
+
? 'reconcile-required'
|
|
85
|
+
: 'not-applicable',
|
|
86
|
+
requiresReconciliation,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function normalizeFieldMask(fieldMask) {
|
|
90
|
+
const unique = new Set(fieldMask);
|
|
91
|
+
if (fieldMask.length === 0 || unique.size !== fieldMask.length) {
|
|
92
|
+
throw new Error('Verification field mask must be non-empty and unique.');
|
|
93
|
+
}
|
|
94
|
+
return FIELD_ORDER.filter((field) => unique.has(field));
|
|
95
|
+
}
|
|
96
|
+
function hashCanonical(value) {
|
|
97
|
+
return `sha256:${createHash('sha256')
|
|
98
|
+
.update(JSON.stringify(value))
|
|
99
|
+
.digest('hex')}`;
|
|
100
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAkB9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAkB9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AA6BzB,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiC5C,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAkyFD,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CA0GT"}
|
|
@@ -8,6 +8,7 @@ import { normalizeDispatchMatrix } from '../../../config/dispatch-matrix.js';
|
|
|
8
8
|
import { effectiveTerminalReviewerTarget, formatDispatchNotices, terminalReviewerNoticeForTarget, } from '../../../config/dispatch-notices.js';
|
|
9
9
|
import { getDispatchPolicyChoices, managedDispatchPolicyValueList, renderDispatchPolicyChoicesMarkdown, } from '../../../config/dispatch-policy-options.js';
|
|
10
10
|
import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, toWorkflowDispatchCandidateLadder, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
|
|
11
|
+
import { getOwnKey, setOwnKey } from '../../../config/own-keys.js';
|
|
11
12
|
import { resolveEffectiveConfig, } from '../../../config/resolve.js';
|
|
12
13
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
13
14
|
import TOML from '@iarna/toml';
|
|
@@ -141,13 +142,17 @@ function isProjectMatrixRecord(value) {
|
|
|
141
142
|
}
|
|
142
143
|
function toProjectMatrixCompatibility(rawMatrix, matrix) {
|
|
143
144
|
const compatibility = {};
|
|
145
|
+
// The normalized matrix now keeps a provider named `__proto__` as an own
|
|
146
|
+
// key, so `Object.entries` yields it here: assignment would drop a scalar
|
|
147
|
+
// entry and install a tier map as this output's prototype.
|
|
144
148
|
for (const [provider, providerValue] of Object.entries(matrix)) {
|
|
145
149
|
if (typeof providerValue === 'string') {
|
|
146
|
-
compatibility
|
|
150
|
+
setOwnKey(compatibility, provider, providerValue);
|
|
147
151
|
continue;
|
|
148
152
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
153
|
+
const rawProviderValue = getOwnKey(rawMatrix, provider);
|
|
154
|
+
const rawProvider = isProjectMatrixRecord(rawProviderValue)
|
|
155
|
+
? rawProviderValue
|
|
151
156
|
: {};
|
|
152
157
|
const tiers = {};
|
|
153
158
|
for (const [tier, cell] of Object.entries(providerValue)) {
|
|
@@ -167,7 +172,7 @@ function toProjectMatrixCompatibility(rawMatrix, matrix) {
|
|
|
167
172
|
}
|
|
168
173
|
tiers[matrixTier] = cell;
|
|
169
174
|
}
|
|
170
|
-
compatibility
|
|
175
|
+
setOwnKey(compatibility, provider, tiers);
|
|
171
176
|
}
|
|
172
177
|
return compatibility;
|
|
173
178
|
}
|
|
@@ -346,7 +351,7 @@ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix
|
|
|
346
351
|
{ source: 'project-state', providers: projectMatrix },
|
|
347
352
|
];
|
|
348
353
|
for (const layer of layers) {
|
|
349
|
-
const resolved = resolveProviderCellFromValue(provider, layer.providers
|
|
354
|
+
const resolved = resolveProviderCellFromValue(provider, layer.providers ? getOwnKey(layer.providers, provider) : undefined, tier, layer.source, escalationLevel);
|
|
350
355
|
if (resolved) {
|
|
351
356
|
selected = resolved;
|
|
352
357
|
}
|
|
@@ -371,7 +376,9 @@ function resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, pro
|
|
|
371
376
|
{ source: 'project-state', providers: projectMatrix },
|
|
372
377
|
];
|
|
373
378
|
for (const layer of layers) {
|
|
374
|
-
const providerValue = layer.providers
|
|
379
|
+
const providerValue = layer.providers
|
|
380
|
+
? getOwnKey(layer.providers, provider)
|
|
381
|
+
: undefined;
|
|
375
382
|
if (typeof providerValue === 'string') {
|
|
376
383
|
selected = null;
|
|
377
384
|
continue;
|
|
@@ -1305,17 +1312,20 @@ function mergeEffectiveDispatchMatrix(resolvedConfig) {
|
|
|
1305
1312
|
resolvedConfig.local.workflow?.dispatchCeiling?.providers,
|
|
1306
1313
|
];
|
|
1307
1314
|
for (const providers of layers) {
|
|
1315
|
+
// Provider names come from user config, which now preserves a key named
|
|
1316
|
+
// `__proto__` as an own key, so `Object.entries` yields it here. Plain
|
|
1317
|
+
// member access would reinstall it as this map's prototype and lose it.
|
|
1308
1318
|
for (const [provider, value] of Object.entries(providers ?? {})) {
|
|
1309
|
-
const previous = merged
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
+
const previous = getOwnKey(merged, provider);
|
|
1320
|
+
const mergedValue = typeof value === 'object' &&
|
|
1321
|
+
value !== null &&
|
|
1322
|
+
!Array.isArray(value) &&
|
|
1323
|
+
typeof previous === 'object' &&
|
|
1324
|
+
previous !== null &&
|
|
1325
|
+
!Array.isArray(previous)
|
|
1326
|
+
? { ...previous, ...value }
|
|
1327
|
+
: value;
|
|
1328
|
+
setOwnKey(merged, provider, mergedValue);
|
|
1319
1329
|
}
|
|
1320
1330
|
}
|
|
1321
1331
|
return merged;
|
|
@@ -1648,7 +1658,9 @@ function writeHumanResolution(context, resolution) {
|
|
|
1648
1658
|
context.logger.info(`${label} dispatch policy: ${policyLabel(resolution.policy)}`);
|
|
1649
1659
|
context.logger.info(`Resolved cap: ${resolution.value ?? 'none'}`);
|
|
1650
1660
|
context.logger.info(`Source: ${sourceLabel(resolution.source)}`);
|
|
1651
|
-
|
|
1661
|
+
// `resolution.provider` is an open string union carrying a user-supplied
|
|
1662
|
+
// `--provider` value, so an unowned prototype name must read as absent.
|
|
1663
|
+
const providerResolution = getOwnKey(resolution.providers, resolution.provider);
|
|
1652
1664
|
if (providerResolution) {
|
|
1653
1665
|
context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
|
|
1654
1666
|
context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
|
|
@@ -1760,7 +1772,9 @@ function buildResolutionReport(resolution, options) {
|
|
|
1760
1772
|
}
|
|
1761
1773
|
const action = options.reportAction;
|
|
1762
1774
|
const role = reportRole(action);
|
|
1763
|
-
|
|
1775
|
+
// `resolution.provider` is an open string union carrying a user-supplied
|
|
1776
|
+
// `--provider` value, so an unowned prototype name must read as absent.
|
|
1777
|
+
const providerResolution = getOwnKey(resolution.providers, resolution.provider);
|
|
1764
1778
|
if (!providerResolution) {
|
|
1765
1779
|
throw new Error(`Dispatch report resolution is missing provider data for "${resolution.provider}".`);
|
|
1766
1780
|
}
|
|
@@ -2,6 +2,7 @@ import { type CommandContext, type GlobalOptions } from '../../../app/command-co
|
|
|
2
2
|
import { type ActiveProjectResolution } from '../../../config/oat-config.js';
|
|
3
3
|
import { type ResolvedConfig } from '../../../config/resolve.js';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { type ProjectLogSeal } from './check.js';
|
|
5
6
|
export declare const PROJECT_LOG_FILENAME = "project-log.md";
|
|
6
7
|
/**
|
|
7
8
|
* Directory, relative to a project, that holds gate partial-finalization
|
|
@@ -34,6 +35,25 @@ export interface ProjectLogLockDependencies {
|
|
|
34
35
|
now: () => number;
|
|
35
36
|
}
|
|
36
37
|
export declare function projectLogLockPath(logPath: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Runs `mutate` with the log's advisory lock held, releasing it on every path.
|
|
40
|
+
*
|
|
41
|
+
* `required` is for mutations that rewrite the file. Running one of those
|
|
42
|
+
* without the lock is exactly the read/modify/write race this exists to close,
|
|
43
|
+
* so an unavailable lock refuses rather than proceeding: a loud, retryable
|
|
44
|
+
* failure that the caller reports is strictly better than a silent clobber of
|
|
45
|
+
* another writer's entry, and the recovery command replays the append safely.
|
|
46
|
+
*
|
|
47
|
+
* `best-effort` is for work that cannot lose an entry — committing does not
|
|
48
|
+
* rewrite the log — where holding the lock only keeps this process from racing
|
|
49
|
+
* its own sibling into `.git/index.lock`.
|
|
50
|
+
*
|
|
51
|
+
* Exported so `synthesize` enters the same critical section rather than a
|
|
52
|
+
* second one of its own: its whole-file rewrite races an overlapping append
|
|
53
|
+
* exactly as two appends race each other, and two locks would serialize
|
|
54
|
+
* neither.
|
|
55
|
+
*/
|
|
56
|
+
export declare function withProjectLogLock<T>(logPath: string, overrides: Partial<ProjectLogLockDependencies>, policy: 'required' | 'best-effort', mutate: () => Promise<T>): Promise<T>;
|
|
37
57
|
export type GitLockFailureClass = 'transient-index-lock' | 'persistent-index-lock' | 'other';
|
|
38
58
|
export type ProjectLogCommitOutcome = 'committed' | 'already-committed' | 'nothing-to-commit' | 'not-a-repo' | 'blocked-by-index-lock' | 'entry-missing-after-commit' | 'commit-unverified' | 'failed';
|
|
39
59
|
export interface ProjectLogCommitResult {
|
|
@@ -186,11 +206,39 @@ export interface AppendProjectLogInput {
|
|
|
186
206
|
versionNote?: string;
|
|
187
207
|
idempotencyKey?: string;
|
|
188
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Raised when an append that is not the completion seal targets a sealed log.
|
|
211
|
+
*
|
|
212
|
+
* This is a throw rather than a fourth `ProjectLogAppendResult` variant because
|
|
213
|
+
* every in-tree caller of `appendProjectLog` runs *before* the seal: the gate's
|
|
214
|
+
* partial-finalization receipt narrows the result's `status` into its own
|
|
215
|
+
* `'appended' | 'already-appended'` field, so widening the union would force a
|
|
216
|
+
* change on a caller that can never legitimately reach this state. Throwing
|
|
217
|
+
* refuses that caller too — the command layer maps this to
|
|
218
|
+
* `status: 'sealed'` with a non-zero exit, and any other caller fails loudly
|
|
219
|
+
* instead of silently appending past the seal.
|
|
220
|
+
*/
|
|
221
|
+
export declare class ProjectLogSealedError extends Error {
|
|
222
|
+
readonly logPath: string;
|
|
223
|
+
readonly seal: ProjectLogSeal;
|
|
224
|
+
/**
|
|
225
|
+
* `action` names the refused mutation so `synthesize`, which rewrites rather
|
|
226
|
+
* than appends, reports what it actually declined. It defaults to `append`,
|
|
227
|
+
* which is the wording every existing caller and its pinned control expect.
|
|
228
|
+
*/
|
|
229
|
+
constructor(logPath: string, seal: ProjectLogSeal, action?: string);
|
|
230
|
+
}
|
|
189
231
|
export type ProjectLogAppendResult = {
|
|
190
232
|
status: 'appended';
|
|
191
233
|
logPath: string;
|
|
192
234
|
heading: string;
|
|
193
235
|
created: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Present and true when the accepted `--body` carried CRLF and was stored
|
|
238
|
+
* as LF. The command reports it rather than normalizing silently, because
|
|
239
|
+
* the bytes on disk then differ from the bytes the caller passed.
|
|
240
|
+
*/
|
|
241
|
+
normalizedLineEndings?: true;
|
|
194
242
|
} | {
|
|
195
243
|
status: 'already-appended';
|
|
196
244
|
logPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/append.ts"],"names":[],"mappings":"AA8BA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAI9E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/append.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,7 +12,8 @@ import { resolveAssetsRoot } from '../../../fs/assets.js';
|
|
|
12
12
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
13
13
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
14
14
|
import { Command } from 'commander';
|
|
15
|
-
import {
|
|
15
|
+
import { findProjectLogAmbiguity, findProjectLogSeal, unreachableProjectLogSealError, } from './check.js';
|
|
16
|
+
import { composeJudgmentHeading, composeStructuralHeading, containsAmbiguousProjectLogMarker, isProjectLogEntryMarker, findProjectLogSealHeadingLine, isProjectLogSealHeading, isProjectLogSectionMarker, PROJECT_LOG_AREA_MAX_LENGTH, PROJECT_LOG_HEADING_DELIMITER, PROJECT_LOG_LINE_TERMINATOR_RE, PROJECT_LOG_LONE_TERMINATOR_RE, PROJECT_LOG_SCOPES, PROJECT_LOG_TYPES, normalizeProjectLogLineEndings, splitProjectLogLines, } from './grammar.js';
|
|
16
17
|
export const PROJECT_LOG_FILENAME = 'project-log.md';
|
|
17
18
|
const SYNTHESIS_HEADING_PREFIX = '\n## End-of-run synthesis';
|
|
18
19
|
/**
|
|
@@ -331,8 +332,13 @@ function releaseProjectLogLock(handle) {
|
|
|
331
332
|
* `best-effort` is for work that cannot lose an entry — committing does not
|
|
332
333
|
* rewrite the log — where holding the lock only keeps this process from racing
|
|
333
334
|
* its own sibling into `.git/index.lock`.
|
|
335
|
+
*
|
|
336
|
+
* Exported so `synthesize` enters the same critical section rather than a
|
|
337
|
+
* second one of its own: its whole-file rewrite races an overlapping append
|
|
338
|
+
* exactly as two appends race each other, and two locks would serialize
|
|
339
|
+
* neither.
|
|
334
340
|
*/
|
|
335
|
-
async function withProjectLogLock(logPath, overrides, policy, mutate) {
|
|
341
|
+
export async function withProjectLogLock(logPath, overrides, policy, mutate) {
|
|
336
342
|
const handle = await acquireProjectLogLock(logPath, overrides);
|
|
337
343
|
if (!handle.held && policy === 'required') {
|
|
338
344
|
throw new Error(`Timed out waiting for the project log lock at ${handle.path}. Another writer is holding it; retry once it finishes.`);
|
|
@@ -794,6 +800,33 @@ export function findProjectLogEntryByIdempotencyKey(content, key, body) {
|
|
|
794
800
|
}
|
|
795
801
|
return undefined;
|
|
796
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Raised when an append that is not the completion seal targets a sealed log.
|
|
805
|
+
*
|
|
806
|
+
* This is a throw rather than a fourth `ProjectLogAppendResult` variant because
|
|
807
|
+
* every in-tree caller of `appendProjectLog` runs *before* the seal: the gate's
|
|
808
|
+
* partial-finalization receipt narrows the result's `status` into its own
|
|
809
|
+
* `'appended' | 'already-appended'` field, so widening the union would force a
|
|
810
|
+
* change on a caller that can never legitimately reach this state. Throwing
|
|
811
|
+
* refuses that caller too — the command layer maps this to
|
|
812
|
+
* `status: 'sealed'` with a non-zero exit, and any other caller fails loudly
|
|
813
|
+
* instead of silently appending past the seal.
|
|
814
|
+
*/
|
|
815
|
+
export class ProjectLogSealedError extends Error {
|
|
816
|
+
logPath;
|
|
817
|
+
seal;
|
|
818
|
+
/**
|
|
819
|
+
* `action` names the refused mutation so `synthesize`, which rewrites rather
|
|
820
|
+
* than appends, reports what it actually declined. It defaults to `append`,
|
|
821
|
+
* which is the wording every existing caller and its pinned control expect.
|
|
822
|
+
*/
|
|
823
|
+
constructor(logPath, seal, action = 'append') {
|
|
824
|
+
super(`No project-log ${action} may follow the completion seal (${seal.heading}) in ${logPath}.`);
|
|
825
|
+
this.name = 'ProjectLogSealedError';
|
|
826
|
+
this.logPath = logPath;
|
|
827
|
+
this.seal = seal;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
797
830
|
const DEFAULT_APPEND_DEPENDENCIES = {
|
|
798
831
|
resolveActiveProject,
|
|
799
832
|
resolveEffectiveConfig,
|
|
@@ -825,7 +858,7 @@ function validateSingleLine(value, option, maxLength = PROJECT_LOG_AREA_MAX_LENG
|
|
|
825
858
|
if (!normalized) {
|
|
826
859
|
throw new Error(`${option} is required and must be a non-empty single line.`);
|
|
827
860
|
}
|
|
828
|
-
if (
|
|
861
|
+
if (PROJECT_LOG_LINE_TERMINATOR_RE.test(value ?? '')) {
|
|
829
862
|
throw new Error(`${option} must be a single line without newline characters.`);
|
|
830
863
|
}
|
|
831
864
|
if (normalized.length > maxLength) {
|
|
@@ -846,16 +879,43 @@ function validateVersionNote(value) {
|
|
|
846
879
|
}
|
|
847
880
|
return validateSingleLine(value, '--version-note', Number.MAX_SAFE_INTEGER);
|
|
848
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Whether any line of `body` is a `## ` or `### ` marker the command owns.
|
|
884
|
+
*
|
|
885
|
+
* The split is `splitProjectLogLines` rather than `/\r?\n/` so validation
|
|
886
|
+
* enumerates exactly the LineTerminators a regular expression's multiline `^`
|
|
887
|
+
* anchors after. Splitting on fewer of them was the injection: a body reading
|
|
888
|
+
* `carrier<CR>## Injected` was one unbroken line here and so passed, while the
|
|
889
|
+
* section parser saw a real `## ` boundary and truncated the entries region,
|
|
890
|
+
* voiding an already-written completion seal.
|
|
891
|
+
*/
|
|
849
892
|
function containsCommandOwnedMarker(body) {
|
|
850
|
-
return body
|
|
851
|
-
.split(/\r?\n/)
|
|
852
|
-
.some((line) => isProjectLogSectionMarker(line) || isProjectLogEntryMarker(line));
|
|
893
|
+
return splitProjectLogLines(body).some((line) => isProjectLogSectionMarker(line) || isProjectLogEntryMarker(line));
|
|
853
894
|
}
|
|
854
895
|
function validateEntry(input, date) {
|
|
855
|
-
|
|
856
|
-
|
|
896
|
+
// The lone-terminator rule is tested against the *raw* body, before trimming.
|
|
897
|
+
// `String.prototype.trim` strips CR, LF, U+2028 and U+2029 as whitespace, so a
|
|
898
|
+
// body edge-terminated by one of them — `'ab\r'`, `'\rabc'` — never reached
|
|
899
|
+
// the test and appended with the terminator silently removed, while the error
|
|
900
|
+
// text said such a body is not accepted. Refusing it narrows what is accepted,
|
|
901
|
+
// which is the safe direction, and makes the message true.
|
|
902
|
+
const raw = input.body ?? '';
|
|
903
|
+
const supplied = raw.trim();
|
|
904
|
+
if (!supplied) {
|
|
857
905
|
throw new Error('--body is required and must contain non-whitespace text.');
|
|
858
906
|
}
|
|
907
|
+
// Parse leniently, write strictly: CRLF is accepted from the caller and stored
|
|
908
|
+
// as LF, so no log this command writes ever carries a terminator other than
|
|
909
|
+
// LF.
|
|
910
|
+
//
|
|
911
|
+
// The lone-terminator rule below is deliberately applied to `raw`: before
|
|
912
|
+
// trimming, and before normalization. Normalizing first would launder `\r\r\n` — a lone CR
|
|
913
|
+
// followed by a CRLF — into `\r\n`, which passes the rule and leaves a CR in
|
|
914
|
+
// the stored bytes, because one `replaceAll` pass cannot re-examine the CR it
|
|
915
|
+
// just exposed. Refusing at the door means every CR that reaches
|
|
916
|
+
// normalization is already part of a CRLF, and one pass is then exact.
|
|
917
|
+
const normalized = supplied.includes('\r\n');
|
|
918
|
+
const body = normalizeProjectLogLineEndings(supplied);
|
|
859
919
|
const versionNote = validateVersionNote(input.versionNote);
|
|
860
920
|
if (input.structural) {
|
|
861
921
|
if (input.type !== undefined ||
|
|
@@ -863,7 +923,7 @@ function validateEntry(input, date) {
|
|
|
863
923
|
input.area !== undefined) {
|
|
864
924
|
throw new Error('--structural cannot be combined with judgment flags --type, --scope, or --area.');
|
|
865
925
|
}
|
|
866
|
-
if (
|
|
926
|
+
if (PROJECT_LOG_LINE_TERMINATOR_RE.test(body)) {
|
|
867
927
|
throw new Error('--body for structural entries must be one line without newline characters.');
|
|
868
928
|
}
|
|
869
929
|
if (containsCommandOwnedMarker(body)) {
|
|
@@ -875,6 +935,7 @@ function validateEntry(input, date) {
|
|
|
875
935
|
heading: composeStructuralHeading({ date, producer, ref }),
|
|
876
936
|
body,
|
|
877
937
|
versionNote,
|
|
938
|
+
normalized,
|
|
878
939
|
};
|
|
879
940
|
}
|
|
880
941
|
if (input.producer !== undefined || input.ref !== undefined) {
|
|
@@ -892,6 +953,12 @@ function validateEntry(input, date) {
|
|
|
892
953
|
if (!PROJECT_LOG_SCOPES.includes(input.scope)) {
|
|
893
954
|
throw new Error(`Invalid --scope '${input.scope}'; accepted values: ${PROJECT_LOG_SCOPES.join(' | ')}.`);
|
|
894
955
|
}
|
|
956
|
+
// Tested against `raw`: before trimming, and before normalization. Trimming
|
|
957
|
+
// hides an edge terminator; normalizing first launders `\r\r\n` into
|
|
958
|
+
// `\r\n`, which passes this rule and leaves a carriage return in the bytes.
|
|
959
|
+
if (PROJECT_LOG_LONE_TERMINATOR_RE.test(raw)) {
|
|
960
|
+
throw new Error('--body for judgment entries must break lines with line feeds or CRLF; a lone carriage return and the U+2028 and U+2029 separators are not accepted.');
|
|
961
|
+
}
|
|
895
962
|
if (containsCommandOwnedMarker(body)) {
|
|
896
963
|
throw new Error('--body for judgment entries must not contain command-owned level-two or level-three Markdown headings.');
|
|
897
964
|
}
|
|
@@ -905,6 +972,7 @@ function validateEntry(input, date) {
|
|
|
905
972
|
}),
|
|
906
973
|
body,
|
|
907
974
|
versionNote,
|
|
975
|
+
normalized,
|
|
908
976
|
};
|
|
909
977
|
}
|
|
910
978
|
/**
|
|
@@ -1000,30 +1068,114 @@ async function appendLockedProjectLog(input, dependencies, logPath) {
|
|
|
1000
1068
|
}
|
|
1001
1069
|
}
|
|
1002
1070
|
const date = dependencies.now().toISOString().slice(0, 10);
|
|
1003
|
-
const { heading, body, versionNote } = validateEntry(input, date);
|
|
1071
|
+
const { heading, body, versionNote, normalized } = validateEntry(input, date);
|
|
1004
1072
|
const idempotencyKey = validateIdempotencyKey(input.idempotencyKey, body);
|
|
1005
1073
|
let created = false;
|
|
1006
|
-
if (logExists
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1074
|
+
if (logExists) {
|
|
1075
|
+
const content = await dependencies.readLog(logPath);
|
|
1076
|
+
// A log whose markers two readers resolve differently cannot answer
|
|
1077
|
+
// "is this sealed?", so nothing may be written onto it until a human
|
|
1078
|
+
// resolves it. This refusal is what keeps the LF-only section parser from
|
|
1079
|
+
// widening such a log: without it, a pre-existing `preamble<U+2028>##
|
|
1080
|
+
// Entries` stops parsing, reports unsealed, and accepts a second seal.
|
|
1081
|
+
const ambiguity = findProjectLogAmbiguity(content, logPath);
|
|
1082
|
+
if (ambiguity !== undefined) {
|
|
1083
|
+
throw new Error(ambiguity);
|
|
1084
|
+
}
|
|
1085
|
+
const seal = findProjectLogSeal(content);
|
|
1086
|
+
// The parser found no seal, but the raw text may still hold a seal heading
|
|
1087
|
+
// it cannot reach — a log with no `## Entries` section, or a seal below
|
|
1088
|
+
// another `## ` section. Writing onto such a log is the outcome that must
|
|
1089
|
+
// never happen, and reporting it unsealed would contradict what is plainly
|
|
1090
|
+
// in the file, so both branches below fail closed on it.
|
|
1091
|
+
//
|
|
1092
|
+
// Refusing is never weaker than the reading it replaces: the pre-fix code
|
|
1093
|
+
// answered these logs `already-appended` and wrote nothing when a key
|
|
1094
|
+
// happened to match, and appended past the seal when it did not.
|
|
1095
|
+
const unreachableSeal = seal === null ? findProjectLogSealHeadingLine(content) : undefined;
|
|
1096
|
+
// Read the requested identity back out of the composed heading, so the
|
|
1097
|
+
// comparison runs on the same normalized producer/ref the parser reads out
|
|
1098
|
+
// of the log.
|
|
1099
|
+
const requestsSeal = isProjectLogSealHeading(heading);
|
|
1100
|
+
// The seal routes on structure before the generic keyed short-circuit, and
|
|
1101
|
+
// is never suppressed by it. `idempotencyToken` matches any whole word
|
|
1102
|
+
// equal to the key *anywhere* in the log, so with the keyed scan first a
|
|
1103
|
+
// single ordinary entry whose prose happened to mention `oat-seal:<project>`
|
|
1104
|
+
// answered the completion seal with `already-appended` naming that entry —
|
|
1105
|
+
// no seal was written, the log stayed open to every later append, and the
|
|
1106
|
+
// completing agent saw success. Structural recognition cannot collide that
|
|
1107
|
+
// way, and the keyed fallback below is admitted only when it names a seal,
|
|
1108
|
+
// so an `already-appended` seal always reports a seal heading.
|
|
1109
|
+
if (requestsSeal) {
|
|
1110
|
+
if (seal !== null) {
|
|
1111
|
+
// The seal is idempotent by structure, not only by key: a log sealed
|
|
1112
|
+
// before the keyed convention carries no token to match, and a resumed
|
|
1113
|
+
// completion must still observe the existing seal rather than write a
|
|
1114
|
+
// second one.
|
|
1115
|
+
return {
|
|
1116
|
+
status: 'already-appended',
|
|
1117
|
+
logPath,
|
|
1118
|
+
heading: seal.heading,
|
|
1119
|
+
created: false,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (unreachableSeal !== undefined) {
|
|
1123
|
+
throw unreachableProjectLogSealError(logPath, content, unreachableSeal);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
// Recognizing an entry this key already wrote comes before the sealed
|
|
1128
|
+
// guard, and deliberately so: it appends nothing, and the entry it finds
|
|
1129
|
+
// necessarily predates any later seal. Refusing it would turn the gate's
|
|
1130
|
+
// idempotent recovery replay into a hard failure on every project that
|
|
1131
|
+
// has since been completed.
|
|
1132
|
+
if (idempotencyKey !== undefined) {
|
|
1133
|
+
const existing = findProjectLogEntryByIdempotencyKey(content, idempotencyKey, body);
|
|
1134
|
+
if (existing !== undefined) {
|
|
1135
|
+
// Append-only order is preserved (DR-260714): a replay observes the
|
|
1136
|
+
// entry it already wrote instead of writing a second one.
|
|
1137
|
+
return {
|
|
1138
|
+
status: 'already-appended',
|
|
1139
|
+
logPath,
|
|
1140
|
+
heading: existing,
|
|
1141
|
+
created: false,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (seal !== null) {
|
|
1146
|
+
throw new ProjectLogSealedError(logPath, seal);
|
|
1147
|
+
}
|
|
1148
|
+
// The same refusal the seal path makes, on the same file. Leaving it out
|
|
1149
|
+
// of this branch is what let a log whose seal the parser could not reach
|
|
1150
|
+
// hard-fail the completion seal while still accepting ordinary content
|
|
1151
|
+
// past that seal — the two writers disagreeing about one file.
|
|
1152
|
+
if (unreachableSeal !== undefined) {
|
|
1153
|
+
throw unreachableProjectLogSealError(logPath, content, unreachableSeal);
|
|
1154
|
+
}
|
|
1017
1155
|
}
|
|
1018
1156
|
}
|
|
1019
1157
|
if (!logExists) {
|
|
1020
1158
|
const assetsRoot = await dependencies.resolveAssetsRoot();
|
|
1021
1159
|
const template = await readFile(join(assetsRoot, 'templates', PROJECT_LOG_FILENAME), 'utf8');
|
|
1022
|
-
|
|
1160
|
+
const instantiated = instantiateProjectLogTemplate(template, basename(dirname(logPath)), date);
|
|
1161
|
+
// The project directory's basename lands in the template's title line, so a
|
|
1162
|
+
// directory named `demo<U+2028>## Notes` would create a log this command
|
|
1163
|
+
// then refuses on its next append. A mutator must never write content it
|
|
1164
|
+
// would decline to read.
|
|
1165
|
+
if (containsAmbiguousProjectLogMarker(instantiated)) {
|
|
1166
|
+
throw new Error(`Project name ${basename(dirname(logPath))} would put a '## ' heading after a carriage return, U+2028, or U+2029 in the new log's title, leaving a file with two readings. Rename the project directory using line feeds only.`);
|
|
1167
|
+
}
|
|
1168
|
+
await dependencies.writeLog(logPath, instantiated);
|
|
1023
1169
|
created = true;
|
|
1024
1170
|
}
|
|
1025
1171
|
await appendEntry(logPath, heading, body, versionNote, dependencies);
|
|
1026
|
-
return {
|
|
1172
|
+
return {
|
|
1173
|
+
status: 'appended',
|
|
1174
|
+
logPath,
|
|
1175
|
+
heading,
|
|
1176
|
+
created,
|
|
1177
|
+
...(normalized ? { normalizedLineEndings: true } : {}),
|
|
1178
|
+
};
|
|
1027
1179
|
}
|
|
1028
1180
|
async function samePath(left, right) {
|
|
1029
1181
|
if (resolve(left) === resolve(right)) {
|
|
@@ -1169,12 +1321,35 @@ async function runAppendCommand(options, context, dependencies) {
|
|
|
1169
1321
|
receiptPath = candidate;
|
|
1170
1322
|
}
|
|
1171
1323
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1324
|
+
let result;
|
|
1325
|
+
try {
|
|
1326
|
+
result = await appendProjectLog({
|
|
1327
|
+
repoRoot,
|
|
1328
|
+
home: context.home,
|
|
1329
|
+
...options,
|
|
1330
|
+
body,
|
|
1331
|
+
}, dependencies);
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
if (!(error instanceof ProjectLogSealedError)) {
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
// A terminal refusal, reported in the same shape as the error path below
|
|
1338
|
+
// so a caller reading JSON can tell a sealed log from a malformed append.
|
|
1339
|
+
if (context.json) {
|
|
1340
|
+
context.logger.json({
|
|
1341
|
+
status: 'sealed',
|
|
1342
|
+
logPath: error.logPath,
|
|
1343
|
+
heading: error.seal.heading,
|
|
1344
|
+
message: error.message,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
context.logger.error(error.message);
|
|
1349
|
+
}
|
|
1350
|
+
process.exitCode = 1;
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1178
1353
|
let commit;
|
|
1179
1354
|
let receiptRemoved = false;
|
|
1180
1355
|
if (options.commit && result.status !== 'skipped') {
|
|
@@ -1249,7 +1424,9 @@ Entry contract:
|
|
|
1249
1424
|
High-value judgments may use Observation:, Impact:, and Recommendation: fields.
|
|
1250
1425
|
Structural bodies are one line and reference artifacts by path instead of inlining them.
|
|
1251
1426
|
Add --version-note for tool-related observations.
|
|
1252
|
-
Pass --idempotency-key
|
|
1427
|
+
Pass --idempotency-key so a replay reports already-appended instead of duplicating the entry; the key must appear in --body as its own word. Adding --commit also finalizes a gate partial-finalization receipt.
|
|
1428
|
+
The completion seal (--structural --producer oat-project-complete --ref seal) dedupes on its own, and every append carrying new content onto a sealed log is refused.
|
|
1429
|
+
A replay recognized by its own --idempotency-key still reports already-appended on a sealed log and writes nothing.
|
|
1253
1430
|
Never record secret values (tokens, keys, signed URLs, or credentials); reference secrets by name or source.
|
|
1254
1431
|
Prior entries are never edited or struck through. Append a new judgment entry that references and explains a correction.
|
|
1255
1432
|
|