@open-agent-toolkit/cli 0.2.66 → 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 +14 -1
- package/assets/docs/cli-utilities/project-log.md +63 -4
- 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/workflows/projects/lifecycle.md +17 -0
- package/assets/docs/workflows/projects/picking-up-projects.md +18 -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 +1 -4
- 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/config/index.d.ts +2 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +133 -38
- 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/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/config/dispatch-matrix.d.ts.map +1 -1
- package/dist/config/dispatch-matrix.js +13 -5
- package/dist/config/oat-config.d.ts +15 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +61 -7
- 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
|
@@ -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
|
|
|
@@ -5,8 +5,39 @@ import { type ProjectLogSection, type ProjectLogScope, type ProjectLogType } fro
|
|
|
5
5
|
export declare const PROJECT_LOG_FILENAME = "project-log.md";
|
|
6
6
|
export declare const SYNTHESIS_PENDING_HEADING = "## End-of-run synthesis (pending \u2014 do not skip at project completion)";
|
|
7
7
|
export declare const SYNTHESIS_COMPLETE_HEADING = "## End-of-run synthesis";
|
|
8
|
+
/**
|
|
9
|
+
* The completion seal a project log already carries.
|
|
10
|
+
*
|
|
11
|
+
* `count` is reported rather than collapsed so a log double-sealed before the
|
|
12
|
+
* seal append became idempotent stays legible instead of looking like a normal
|
|
13
|
+
* single-sealed log; `heading` is always the **first** seal, which is the one
|
|
14
|
+
* that actually closed the log.
|
|
15
|
+
*/
|
|
16
|
+
export interface ProjectLogSeal {
|
|
17
|
+
heading: string;
|
|
18
|
+
date: string;
|
|
19
|
+
keyed: boolean;
|
|
20
|
+
count: number;
|
|
21
|
+
}
|
|
8
22
|
export interface ProjectLogCheckResult {
|
|
9
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A *sealed* log deliberately keeps its ordinary status and carries `sealed`
|
|
25
|
+
* alongside it, because it is a healthy log that must keep routing normally —
|
|
26
|
+
* a distinct value there would break every `status: "ok"` route in
|
|
27
|
+
* `oat-project-complete/SKILL.md` and `oat-project-summary/SKILL.md`.
|
|
28
|
+
*
|
|
29
|
+
* `ambiguous` is the opposite case and is the one value that may join them.
|
|
30
|
+
* The log's markers have two readings, so no verdict below can be computed
|
|
31
|
+
* honestly: every other field is zeroed and must be treated as unknown, not as
|
|
32
|
+
* a finding. Breaking the routing is the point — both mutators already refuse
|
|
33
|
+
* this file, and `check` answering `ok` while they refuse is what let the
|
|
34
|
+
* completion gate trust a reader that disagreed with every writer.
|
|
35
|
+
*/
|
|
36
|
+
status: 'ok' | 'absent' | 'synthesis_pending' | 'ambiguous';
|
|
37
|
+
/** Why the log has two readings, or null when it has one. */
|
|
38
|
+
ambiguity: string | null;
|
|
39
|
+
sealed: boolean;
|
|
40
|
+
seal: ProjectLogSeal | null;
|
|
10
41
|
logPath: string | null;
|
|
11
42
|
entryCounts: {
|
|
12
43
|
structural: number;
|
|
@@ -60,7 +91,62 @@ export interface ProjectLogCheckCommandDependencies extends CheckProjectLogDepen
|
|
|
60
91
|
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
61
92
|
}
|
|
62
93
|
export declare function findCanonicalProjectLogSynthesisSection(content: string): ProjectLogSynthesisSection;
|
|
94
|
+
/**
|
|
95
|
+
* Parses the entries region into typed entries.
|
|
96
|
+
*
|
|
97
|
+
* The split is `/\r?\n/`, not `'\n'`. With a bare LF split every line of a
|
|
98
|
+
* CRLF log keeps a trailing `\r`, which no heading pattern can match — their
|
|
99
|
+
* `$` sits after a `[^·\r\n]+` run — so a CRLF log parsed to zero entries and
|
|
100
|
+
* an existing completion seal was invisible to `check` and to the append guard.
|
|
101
|
+
* A *lone* CR is still not a boundary, which is what keeps a body from forging
|
|
102
|
+
* a heading; only the CRLF pair is folded in.
|
|
103
|
+
*/
|
|
63
104
|
export declare function parseProjectLogEntries(content: string): ParsedProjectLog;
|
|
105
|
+
/**
|
|
106
|
+
* Summarizes the completion seal carried by already-parsed entries.
|
|
107
|
+
*
|
|
108
|
+
* Seal identity is `isProjectLogSealEntry` and nothing else, so `check` and
|
|
109
|
+
* `append` cannot drift apart on what "sealed" means.
|
|
110
|
+
*/
|
|
111
|
+
export declare function summarizeProjectLogSeal(entries: readonly ParsedProjectLogEntry[]): ProjectLogSeal | null;
|
|
112
|
+
/**
|
|
113
|
+
* Finds the completion seal in raw project-log content.
|
|
114
|
+
*
|
|
115
|
+
* `append` uses this so its refusal and `check`'s report are the same reading
|
|
116
|
+
* of the same file.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* The reason this log has two readings, or undefined when it has one.
|
|
120
|
+
*
|
|
121
|
+
* The single question `check`, `rollup`, both append paths and `synthesize` all
|
|
122
|
+
* ask before trusting a parse. It is one function rather than one call each,
|
|
123
|
+
* because a surface that asked a slightly different question is exactly how
|
|
124
|
+
* `rollup` came to rewrite `summary.md` from a log the other three refused,
|
|
125
|
+
* reporting output byte-identical to an empty log.
|
|
126
|
+
*
|
|
127
|
+
* Two shapes qualify. A heading that a `^…$/m` reader starts a line at and the
|
|
128
|
+
* line-feed-only parser does not; and a completion seal that is physically in
|
|
129
|
+
* the file but outside the region the parser reads, where "is this sealed?" has
|
|
130
|
+
* one answer to a human and another to the parser.
|
|
131
|
+
*/
|
|
132
|
+
export declare function findProjectLogAmbiguity(content: string, logPath: string): string | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Explains a refusal caused by a seal the entries parser cannot reach, naming
|
|
135
|
+
* the actual obstruction.
|
|
136
|
+
*
|
|
137
|
+
* The first version of this message assumed the only way to hide a seal was to
|
|
138
|
+
* place it outside `## Entries`, and offered that remedy for a CRLF log whose
|
|
139
|
+
* seal sat exactly where it belonged — a remedy that did not apply and could
|
|
140
|
+
* not be followed, on the one path a completion run cannot get past.
|
|
141
|
+
*
|
|
142
|
+
* Shared by both mutators so one file cannot produce two different accounts of
|
|
143
|
+
* why it was refused.
|
|
144
|
+
*/
|
|
145
|
+
export declare function unreachableProjectLogSealError(logPath: string, content: string, seal: {
|
|
146
|
+
line: string;
|
|
147
|
+
index: number;
|
|
148
|
+
}): Error;
|
|
149
|
+
export declare function findProjectLogSeal(content: string): ProjectLogSeal | null;
|
|
64
150
|
export declare function checkProjectLog(input: CheckProjectLogInput, overrides?: Partial<CheckProjectLogDependencies>): Promise<ProjectLogCheckResult>;
|
|
65
151
|
export declare function createProjectLogCheckCommand(overrides?: Partial<ProjectLogCheckCommandDependencies>): Command;
|
|
66
152
|
//# sourceMappingURL=check.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/check.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/check.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { resolveActiveProject, } from '../../../config/oat-config.js';
|
|
|
6
6
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
7
7
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
8
8
|
import { Command } from 'commander';
|
|
9
|
-
import { findProjectLogSections, isProjectLogEntryMarker, JUDGMENT_HEADING_RE, PROJECT_LOG_TYPES, STRUCTURAL_HEADING_RE, } from './grammar.js';
|
|
9
|
+
import { carriesProjectLogSealKey, containsAmbiguousProjectLogMarker, findProjectLogSealHeadingLine, findProjectLogSections, isProjectLogEntryMarker, isProjectLogSealEntry, JUDGMENT_HEADING_RE, PROJECT_LOG_TYPES, STRUCTURAL_HEADING_RE, } from './grammar.js';
|
|
10
10
|
export const PROJECT_LOG_FILENAME = 'project-log.md';
|
|
11
11
|
export const SYNTHESIS_PENDING_HEADING = '## End-of-run synthesis (pending — do not skip at project completion)';
|
|
12
12
|
export const SYNTHESIS_COMPLETE_HEADING = '## End-of-run synthesis';
|
|
@@ -32,8 +32,10 @@ function emptyCounts() {
|
|
|
32
32
|
scopeCounts: { project: 0, general: 0 },
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
/** The heading that opens the region entries are parsed from. */
|
|
36
|
+
const ENTRIES_HEADING = '## Entries';
|
|
35
37
|
function entriesSection(content) {
|
|
36
|
-
const section = findProjectLogSections(content).find(({ heading }) => heading ===
|
|
38
|
+
const section = findProjectLogSections(content).find(({ heading }) => heading === ENTRIES_HEADING);
|
|
37
39
|
if (!section) {
|
|
38
40
|
return '';
|
|
39
41
|
}
|
|
@@ -54,8 +56,18 @@ export function findCanonicalProjectLogSynthesisSection(content) {
|
|
|
54
56
|
section,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Parses the entries region into typed entries.
|
|
61
|
+
*
|
|
62
|
+
* The split is `/\r?\n/`, not `'\n'`. With a bare LF split every line of a
|
|
63
|
+
* CRLF log keeps a trailing `\r`, which no heading pattern can match — their
|
|
64
|
+
* `$` sits after a `[^·\r\n]+` run — so a CRLF log parsed to zero entries and
|
|
65
|
+
* an existing completion seal was invisible to `check` and to the append guard.
|
|
66
|
+
* A *lone* CR is still not a boundary, which is what keeps a body from forging
|
|
67
|
+
* a heading; only the CRLF pair is folded in.
|
|
68
|
+
*/
|
|
57
69
|
export function parseProjectLogEntries(content) {
|
|
58
|
-
const lines = entriesSection(content).split(
|
|
70
|
+
const lines = entriesSection(content).split(/\r?\n/);
|
|
59
71
|
const entries = [];
|
|
60
72
|
const grammarViolations = [];
|
|
61
73
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -101,6 +113,90 @@ export function parseProjectLogEntries(content) {
|
|
|
101
113
|
}
|
|
102
114
|
return { entries, grammarViolations };
|
|
103
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Summarizes the completion seal carried by already-parsed entries.
|
|
118
|
+
*
|
|
119
|
+
* Seal identity is `isProjectLogSealEntry` and nothing else, so `check` and
|
|
120
|
+
* `append` cannot drift apart on what "sealed" means.
|
|
121
|
+
*/
|
|
122
|
+
export function summarizeProjectLogSeal(entries) {
|
|
123
|
+
const seals = entries.filter((entry) => entry.class === 'structural' && isProjectLogSealEntry(entry));
|
|
124
|
+
const first = seals[0];
|
|
125
|
+
if (first === undefined) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
heading: first.heading,
|
|
130
|
+
date: first.date,
|
|
131
|
+
keyed: carriesProjectLogSealKey(first.body),
|
|
132
|
+
count: seals.length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Finds the completion seal in raw project-log content.
|
|
137
|
+
*
|
|
138
|
+
* `append` uses this so its refusal and `check`'s report are the same reading
|
|
139
|
+
* of the same file.
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* The reason this log has two readings, or undefined when it has one.
|
|
143
|
+
*
|
|
144
|
+
* The single question `check`, `rollup`, both append paths and `synthesize` all
|
|
145
|
+
* ask before trusting a parse. It is one function rather than one call each,
|
|
146
|
+
* because a surface that asked a slightly different question is exactly how
|
|
147
|
+
* `rollup` came to rewrite `summary.md` from a log the other three refused,
|
|
148
|
+
* reporting output byte-identical to an empty log.
|
|
149
|
+
*
|
|
150
|
+
* Two shapes qualify. A heading that a `^…$/m` reader starts a line at and the
|
|
151
|
+
* line-feed-only parser does not; and a completion seal that is physically in
|
|
152
|
+
* the file but outside the region the parser reads, where "is this sealed?" has
|
|
153
|
+
* one answer to a human and another to the parser.
|
|
154
|
+
*/
|
|
155
|
+
export function findProjectLogAmbiguity(content, logPath) {
|
|
156
|
+
if (containsAmbiguousProjectLogMarker(content)) {
|
|
157
|
+
return `Project log ${logPath} has a '## ' heading, or a dated '### ' entry heading, starting a line after a carriage return, U+2028, or U+2029 rather than a line feed, so its structure has two readings and no seal or count can be reported honestly. Replace those line terminators with line feeds.`;
|
|
158
|
+
}
|
|
159
|
+
if (findProjectLogSeal(content) === null) {
|
|
160
|
+
const unreachableSeal = findProjectLogSealHeadingLine(content);
|
|
161
|
+
if (unreachableSeal !== undefined) {
|
|
162
|
+
// Delegated so the precise diagnosis — which of the two obstructions this
|
|
163
|
+
// is — survives being reached through the shared predicate. Collapsing
|
|
164
|
+
// both causes into one sentence here would undo the round that made the
|
|
165
|
+
// remedy actionable.
|
|
166
|
+
return unreachableProjectLogSealError(logPath, content, unreachableSeal)
|
|
167
|
+
.message;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Explains a refusal caused by a seal the entries parser cannot reach, naming
|
|
174
|
+
* the actual obstruction.
|
|
175
|
+
*
|
|
176
|
+
* The first version of this message assumed the only way to hide a seal was to
|
|
177
|
+
* place it outside `## Entries`, and offered that remedy for a CRLF log whose
|
|
178
|
+
* seal sat exactly where it belonged — a remedy that did not apply and could
|
|
179
|
+
* not be followed, on the one path a completion run cannot get past.
|
|
180
|
+
*
|
|
181
|
+
* Shared by both mutators so one file cannot produce two different accounts of
|
|
182
|
+
* why it was refused.
|
|
183
|
+
*/
|
|
184
|
+
export function unreachableProjectLogSealError(logPath, content, seal) {
|
|
185
|
+
// Compared by offset, not by substring membership: prose inside `## Entries`
|
|
186
|
+
// can quote a seal heading verbatim, and a membership test would then report a
|
|
187
|
+
// seal that really sits under a later section as correctly placed, sending the
|
|
188
|
+
// operator to rewrite line terminators that were never the obstruction.
|
|
189
|
+
const entries = findProjectLogSections(content).find(({ heading }) => heading === ENTRIES_HEADING);
|
|
190
|
+
const outsideEntries = entries === undefined ||
|
|
191
|
+
seal.index < entries.start ||
|
|
192
|
+
seal.index >= entries.end;
|
|
193
|
+
return new Error(`Project log ${logPath} already carries the completion-seal heading '${seal.line.trim()}', but the entries parser cannot reach it, so whether the log is sealed has two answers and nothing may be read from it or written to it with confidence. ${outsideEntries
|
|
194
|
+
? `The seal sits outside the '${ENTRIES_HEADING}' section; move it under that heading, then retry.`
|
|
195
|
+
: 'The seal is under the correct heading, so the obstruction is its line terminators; rewrite the log with line feeds, then retry.'}`);
|
|
196
|
+
}
|
|
197
|
+
export function findProjectLogSeal(content) {
|
|
198
|
+
return summarizeProjectLogSeal(parseProjectLogEntries(content).entries);
|
|
199
|
+
}
|
|
104
200
|
async function resolveTargetProject(input, dependencies) {
|
|
105
201
|
if (input.project) {
|
|
106
202
|
const projectPath = isAbsolute(input.project)
|
|
@@ -125,6 +221,9 @@ export async function checkProjectLog(input, overrides = {}) {
|
|
|
125
221
|
if (!(await fileExists(logPath))) {
|
|
126
222
|
return {
|
|
127
223
|
status: 'absent',
|
|
224
|
+
ambiguity: null,
|
|
225
|
+
sealed: false,
|
|
226
|
+
seal: null,
|
|
128
227
|
logPath: null,
|
|
129
228
|
...emptyCounts(),
|
|
130
229
|
lastEntryDate: null,
|
|
@@ -133,6 +232,24 @@ export async function checkProjectLog(input, overrides = {}) {
|
|
|
133
232
|
};
|
|
134
233
|
}
|
|
135
234
|
const content = await readFile(logPath, 'utf8');
|
|
235
|
+
// Fail closed, exactly as every other surface does on this same file. `check`
|
|
236
|
+
// used to be the one reader that always produced a confident verdict: on a
|
|
237
|
+
// `preamble<U+2028>## Entries` log it answered `sealed` and count fields the
|
|
238
|
+
// writers refused to act on, and the completion gate keys on those fields.
|
|
239
|
+
const ambiguity = findProjectLogAmbiguity(content, logPath);
|
|
240
|
+
if (ambiguity !== undefined) {
|
|
241
|
+
return {
|
|
242
|
+
status: 'ambiguous',
|
|
243
|
+
ambiguity,
|
|
244
|
+
sealed: false,
|
|
245
|
+
seal: null,
|
|
246
|
+
logPath,
|
|
247
|
+
...emptyCounts(),
|
|
248
|
+
lastEntryDate: null,
|
|
249
|
+
synthesisPending: false,
|
|
250
|
+
grammarViolations: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
136
253
|
const parsed = parseProjectLogEntries(content);
|
|
137
254
|
const counts = emptyCounts();
|
|
138
255
|
for (const entry of parsed.entries) {
|
|
@@ -145,8 +262,12 @@ export async function checkProjectLog(input, overrides = {}) {
|
|
|
145
262
|
}
|
|
146
263
|
}
|
|
147
264
|
const synthesisPending = findProjectLogSections(content).some(({ heading }) => heading === SYNTHESIS_PENDING_HEADING);
|
|
265
|
+
const seal = summarizeProjectLogSeal(parsed.entries);
|
|
148
266
|
return {
|
|
149
267
|
status: synthesisPending ? 'synthesis_pending' : 'ok',
|
|
268
|
+
ambiguity: null,
|
|
269
|
+
sealed: seal !== null,
|
|
270
|
+
seal,
|
|
150
271
|
logPath,
|
|
151
272
|
...counts,
|
|
152
273
|
lastEntryDate: parsed.entries.at(-1)?.date ?? null,
|
|
@@ -161,14 +282,27 @@ async function runCheckCommand(options, context, dependencies) {
|
|
|
161
282
|
if (context.json) {
|
|
162
283
|
context.logger.json(result);
|
|
163
284
|
}
|
|
285
|
+
else if (result.status === 'ambiguous') {
|
|
286
|
+
context.logger.error(result.ambiguity ?? 'Project log has two readings.');
|
|
287
|
+
}
|
|
164
288
|
else {
|
|
165
289
|
context.logger.info(result.status === 'absent'
|
|
166
290
|
? 'Project log: absent'
|
|
167
291
|
: `Project log: ${result.status}; entries: ${result.entryCounts.structural +
|
|
168
|
-
PROJECT_LOG_TYPES.reduce((total, type) => total + result.entryCounts.judgment[type], 0)}
|
|
292
|
+
PROJECT_LOG_TYPES.reduce((total, type) => total + result.entryCounts.judgment[type], 0)}${result.seal
|
|
293
|
+
? `; sealed${result.seal.count > 1
|
|
294
|
+
? ` (${result.seal.count} seal entries)`
|
|
295
|
+
: ''}`
|
|
296
|
+
: ''}`);
|
|
169
297
|
}
|
|
298
|
+
// An ambiguous log exits non-zero whether or not synthesis was required: a
|
|
299
|
+
// caller that only checks the exit status must not read this as a clean log,
|
|
300
|
+
// and both mutators refuse the same file.
|
|
170
301
|
process.exitCode =
|
|
171
|
-
|
|
302
|
+
result.status === 'ambiguous' ||
|
|
303
|
+
(options.requireSynthesis === true && result.synthesisPending)
|
|
304
|
+
? 1
|
|
305
|
+
: 0;
|
|
172
306
|
}
|
|
173
307
|
catch (error) {
|
|
174
308
|
const message = error instanceof Error ? error.message : String(error);
|