@open-agent-toolkit/cli 0.2.75 → 0.2.77
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/config-and-local-state.md +3 -3
- package/assets/docs/cli-utilities/configuration.md +3 -1
- package/assets/docs/cli-utilities/index.md +1 -0
- package/assets/docs/cli-utilities/tool-packs.md +1 -1
- package/assets/docs/reference/file-locations.md +1 -1
- package/assets/docs/reference/oat-directory-structure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +1 -1
- package/assets/skills/explainer-kit/scripts/bundle.mjs +26 -2
- package/assets/skills/explainer-kit/scripts/record.mjs +26 -3
- package/assets/skills/explainer-kit/scripts/verify.mjs +26 -3
- package/assets/skills/oat-doctor/SKILL.md +153 -193
- package/assets/skills/oat-explainer-kit/SKILL.md +1 -1
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +25 -5
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-complete/SKILL.md +1 -1
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +26 -6
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +26 -6
- package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +7 -0
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +26 -5
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +26 -5
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +26 -6
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +26 -6
- package/assets/skills/oat-project-complete/scripts/resolve-synced-archive-entry.mjs +26 -5
- package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +7 -0
- package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs +126 -95
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/scripts/capture-dirty-tree.mjs +3 -0
- 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/SKILL.md +9 -6
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/recon/SKILL.md +1 -1
- package/assets/skills/recon/scripts/create-review-brief.mjs +25 -5
- package/assets/skills/recon/scripts/prepare-routing.mjs +26 -2
- package/assets/skills/recon/scripts/render-packet.mjs +25 -5
- package/assets/skills/recon/scripts/validate-artifact.mjs +25 -5
- package/assets/skills/recon/scripts/validate-packet.mjs +25 -5
- package/assets/templates/implementation.md +1 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +18 -1
- package/dist/config/oat-config.d.ts +1 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
1
2
|
import { readdir, realpath } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import { readPersistedIntent } from '../../oat-explainer-kit/scripts/persist-intent.mjs';
|
|
6
7
|
|
|
@@ -110,10 +111,29 @@ async function main(argv) {
|
|
|
110
111
|
return consumePersistedRecapIntent({ projectPath: argv[0] });
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
116
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
117
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
118
|
+
* and canonicalizing only one side has the same effect under
|
|
119
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
120
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
121
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
122
|
+
* `false`; it never masks a direct run.
|
|
123
|
+
*/
|
|
124
|
+
function isDirectInvocation(invokedPath) {
|
|
125
|
+
if (!invokedPath) return false;
|
|
126
|
+
try {
|
|
127
|
+
return (
|
|
128
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
129
|
+
realpathSync(resolve(invokedPath))
|
|
130
|
+
);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
117
137
|
main(process.argv.slice(2))
|
|
118
138
|
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
|
|
119
139
|
.catch((error) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { isAbsolute, posix, relative, sep } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { isAbsolute, posix, relative, sep, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
|
|
7
8
|
import { validateSyncedArchiveTerminalReport } from './finalize-synced-archive.mjs';
|
|
@@ -261,10 +262,29 @@ async function main(argv) {
|
|
|
261
262
|
});
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
267
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
268
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
269
|
+
* and canonicalizing only one side has the same effect under
|
|
270
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
271
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
272
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
273
|
+
* `false`; it never masks a direct run.
|
|
274
|
+
*/
|
|
275
|
+
function isDirectInvocation(invokedPath) {
|
|
276
|
+
if (!invokedPath) return false;
|
|
277
|
+
try {
|
|
278
|
+
return (
|
|
279
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
280
|
+
realpathSync(resolve(invokedPath))
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
268
288
|
main(process.argv.slice(2))
|
|
269
289
|
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
|
|
270
290
|
.catch((error) => {
|
|
@@ -92,6 +92,10 @@ async function main(argv) {
|
|
|
92
92
|
projectName,
|
|
93
93
|
getArchiveReport: async () => {
|
|
94
94
|
try {
|
|
95
|
+
// Stdin is read to EOF with no timeout. Every caller pipes the archive
|
|
96
|
+
// report in (`printf '%s\n' "$ARCHIVE_OUTPUT" | node ...`), so EOF
|
|
97
|
+
// always arrives; a timeout would turn a slow producer into a false
|
|
98
|
+
// failure, and a caller that leaves stdin open is a caller bug.
|
|
95
99
|
return JSON.parse(readFileSync(0, 'utf8'));
|
|
96
100
|
} catch (error) {
|
|
97
101
|
throw finalizationError(
|
|
@@ -119,6 +123,9 @@ async function main(argv) {
|
|
|
119
123
|
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
|
|
120
124
|
* way the caller reads "exited 0" as "verified" — the fail-open shape this
|
|
121
125
|
* finalizer exists to prevent.
|
|
126
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
127
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
128
|
+
* `false`; it never masks a direct run.
|
|
122
129
|
*/
|
|
123
130
|
function isDirectInvocation(invokedPath) {
|
|
124
131
|
if (!invokedPath) return false;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
|
|
3
5
|
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
4
6
|
const PR_ARTIFACT = /^pr\/project-pr-[^/\s]+\.md$/;
|
|
@@ -217,10 +219,29 @@ function main(argv) {
|
|
|
217
219
|
return parseCompletionRetryFields(argv[0]);
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
224
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
225
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
226
|
+
* and canonicalizing only one side has the same effect under
|
|
227
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
228
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
229
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
230
|
+
* `false`; it never masks a direct run.
|
|
231
|
+
*/
|
|
232
|
+
function isDirectInvocation(invokedPath) {
|
|
233
|
+
if (!invokedPath) return false;
|
|
234
|
+
try {
|
|
235
|
+
return (
|
|
236
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
237
|
+
realpathSync(resolve(invokedPath))
|
|
238
|
+
);
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
224
245
|
try {
|
|
225
246
|
process.stdout.write(`${main(process.argv.slice(2))}\n`);
|
|
226
247
|
} catch (error) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
|
|
3
5
|
function fieldsError(message) {
|
|
4
6
|
const error = new Error(message);
|
|
@@ -92,10 +94,29 @@ function main(argv) {
|
|
|
92
94
|
return formatShellAssignments(parseSyncedArchiveResumeFields(result));
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
99
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
100
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
101
|
+
* and canonicalizing only one side has the same effect under
|
|
102
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
103
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
104
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
105
|
+
* `false`; it never masks a direct run.
|
|
106
|
+
*/
|
|
107
|
+
function isDirectInvocation(invokedPath) {
|
|
108
|
+
if (!invokedPath) return false;
|
|
109
|
+
try {
|
|
110
|
+
return (
|
|
111
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
112
|
+
realpathSync(resolve(invokedPath))
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
99
120
|
try {
|
|
100
121
|
process.stdout.write(`${main(process.argv.slice(2))}\n`);
|
|
101
122
|
} catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { basename, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -692,10 +693,29 @@ async function main(argv) {
|
|
|
692
693
|
return recoverCompletionReceipts(options);
|
|
693
694
|
}
|
|
694
695
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
698
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
699
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
700
|
+
* and canonicalizing only one side has the same effect under
|
|
701
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
702
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
703
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
704
|
+
* `false`; it never masks a direct run.
|
|
705
|
+
*/
|
|
706
|
+
function isDirectInvocation(invokedPath) {
|
|
707
|
+
if (!invokedPath) return false;
|
|
708
|
+
try {
|
|
709
|
+
return (
|
|
710
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
711
|
+
realpathSync(resolve(invokedPath))
|
|
712
|
+
);
|
|
713
|
+
} catch {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
699
719
|
main(process.argv.slice(2))
|
|
700
720
|
.then((result) => {
|
|
701
721
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
1
2
|
import { lstat, readdir } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
detectCompletionReceiptCandidate,
|
|
@@ -116,10 +117,29 @@ async function main(argv) {
|
|
|
116
117
|
return resolveCompletionRetry(parseArguments(argv));
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
122
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
123
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
124
|
+
* and canonicalizing only one side has the same effect under
|
|
125
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
126
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
127
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
128
|
+
* `false`; it never masks a direct run.
|
|
129
|
+
*/
|
|
130
|
+
function isDirectInvocation(invokedPath) {
|
|
131
|
+
if (!invokedPath) return false;
|
|
132
|
+
try {
|
|
133
|
+
return (
|
|
134
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
135
|
+
realpathSync(resolve(invokedPath))
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
123
143
|
main(process.argv.slice(2))
|
|
124
144
|
.then((result) => {
|
|
125
145
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { promisify } from 'node:util';
|
|
5
7
|
|
|
6
8
|
const execFile = promisify(execFileCallback);
|
|
@@ -149,10 +151,29 @@ async function main(argv) {
|
|
|
149
151
|
return resolveSyncedArchiveEntry({ ...options, record });
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
156
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
157
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
158
|
+
* and canonicalizing only one side has the same effect under
|
|
159
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
160
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
161
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
162
|
+
* `false`; it never masks a direct run.
|
|
163
|
+
*/
|
|
164
|
+
function isDirectInvocation(invokedPath) {
|
|
165
|
+
if (!invokedPath) return false;
|
|
166
|
+
try {
|
|
167
|
+
return (
|
|
168
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
169
|
+
realpathSync(resolve(invokedPath))
|
|
170
|
+
);
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
156
177
|
main(process.argv.slice(2))
|
|
157
178
|
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
|
|
158
179
|
.catch((error) => {
|
|
@@ -134,6 +134,10 @@ async function main(argv) {
|
|
|
134
134
|
if (options.mode === 'receipt') {
|
|
135
135
|
let report;
|
|
136
136
|
try {
|
|
137
|
+
// Stdin is read to EOF with no timeout. Every caller pipes the archive
|
|
138
|
+
// report in (`printf '%s\n' "$ARCHIVE_OUTPUT" | node ...`), so EOF
|
|
139
|
+
// always arrives; a timeout would turn a slow producer into a false
|
|
140
|
+
// failure, and a caller that leaves stdin open is a caller bug.
|
|
137
141
|
report = JSON.parse(readFileSync(0, 'utf8'));
|
|
138
142
|
} catch (error) {
|
|
139
143
|
throw receiptError(
|
|
@@ -158,6 +162,9 @@ async function main(argv) {
|
|
|
158
162
|
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
|
|
159
163
|
* way the caller reads "exited 0" as "verified" — the fail-open shape this
|
|
160
164
|
* validator exists to prevent.
|
|
165
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
166
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
167
|
+
* `false`; it never masks a direct run.
|
|
161
168
|
*/
|
|
162
169
|
function isDirectInvocation(invokedPath) {
|
|
163
170
|
if (!invokedPath) return false;
|
package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolve,
|
|
12
12
|
sep,
|
|
13
13
|
} from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
function fail(message) {
|
|
16
17
|
process.stderr.write(`Invalid non-archive lifecycle receipt: ${message}\n`);
|
|
@@ -57,104 +58,134 @@ function gitSucceeds(cwd, args) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
function main(argv) {
|
|
62
|
+
const [recordArgument, commit, projectName] = argv;
|
|
63
|
+
if (!recordArgument || !/^[0-9a-f]{40}$/.test(commit ?? '') || !projectName) {
|
|
64
|
+
fail('expected <record-path> <full-commit-sha> <project-name>.');
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
const unresolvedRecordPath = resolve(recordArgument);
|
|
66
|
-
if (!existsSync(unresolvedRecordPath)) {
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const recordPath = realpathSync(unresolvedRecordPath);
|
|
70
|
-
const repoRoot = realpathSync(
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
const recordRelative = relative(repoRoot, recordPath);
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) {
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const gitRecordPath = recordRelative.split(sep).join('/');
|
|
83
|
-
if (basename(gitRecordPath, extname(gitRecordPath)) !== projectName) {
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (!gitSucceeds(repoRoot, ['merge-base', '--is-ancestor', commit, 'HEAD'])) {
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
const changedPaths = git(repoRoot, [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
])
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (changedPaths.length !== 1 || changedPaths[0] !== gitRecordPath) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
67
|
+
const unresolvedRecordPath = resolve(recordArgument);
|
|
68
|
+
if (!existsSync(unresolvedRecordPath)) {
|
|
69
|
+
fail('record path does not exist; restore the synced discovery record.');
|
|
70
|
+
}
|
|
71
|
+
const recordPath = realpathSync(unresolvedRecordPath);
|
|
72
|
+
const repoRoot = realpathSync(
|
|
73
|
+
git(dirname(recordPath), ['rev-parse', '--show-toplevel']),
|
|
74
|
+
);
|
|
75
|
+
const recordRelative = relative(repoRoot, recordPath);
|
|
76
|
+
if (
|
|
77
|
+
recordRelative === '' ||
|
|
78
|
+
recordRelative === '..' ||
|
|
79
|
+
recordRelative.startsWith(`..${sep}`) ||
|
|
80
|
+
isAbsolute(recordRelative)
|
|
81
|
+
) {
|
|
82
|
+
fail('record path is outside the repository root.');
|
|
83
|
+
}
|
|
84
|
+
const gitRecordPath = recordRelative.split(sep).join('/');
|
|
85
|
+
if (basename(gitRecordPath, extname(gitRecordPath)) !== projectName) {
|
|
86
|
+
fail('record filename must match the project name.');
|
|
87
|
+
}
|
|
88
|
+
if (!gitSucceeds(repoRoot, ['merge-base', '--is-ancestor', commit, 'HEAD'])) {
|
|
89
|
+
fail('receipt commit must be an ancestor of the current repository HEAD.');
|
|
90
|
+
}
|
|
91
|
+
const changedPaths = git(repoRoot, [
|
|
92
|
+
'diff-tree',
|
|
93
|
+
'--root',
|
|
94
|
+
'--no-commit-id',
|
|
95
|
+
'--name-only',
|
|
96
|
+
'-r',
|
|
97
|
+
'--format=',
|
|
98
|
+
commit,
|
|
99
|
+
])
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
if (changedPaths.length !== 1 || changedPaths[0] !== gitRecordPath) {
|
|
103
|
+
fail(
|
|
104
|
+
'receipt commit must change exactly the synced discovery record path.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const expectedSubject = `chore(oat): complete synced project ${projectName}`;
|
|
108
|
+
const subject = git(repoRoot, ['show', '-s', '--format=%s', commit]);
|
|
109
|
+
if (subject !== expectedSubject) {
|
|
110
|
+
fail(`commit subject must be "${expectedSubject}".`);
|
|
111
|
+
}
|
|
108
112
|
|
|
109
|
-
let committedRecordContents;
|
|
110
|
-
let record;
|
|
111
|
-
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
let committedRecordContents;
|
|
114
|
+
let record;
|
|
115
|
+
try {
|
|
116
|
+
committedRecordContents = gitRaw(repoRoot, [
|
|
117
|
+
'show',
|
|
118
|
+
`${commit}:${gitRecordPath}`,
|
|
119
|
+
]);
|
|
120
|
+
record = JSON.parse(committedRecordContents);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fail(
|
|
123
|
+
`committed record is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (readFileSync(recordPath, 'utf8') !== committedRecordContents) {
|
|
127
|
+
fail('working-tree record content must exactly match the receipt commit.');
|
|
128
|
+
}
|
|
129
|
+
const expectedKeys = new Set([
|
|
130
|
+
'archiveSnapshot',
|
|
131
|
+
'completedAt',
|
|
132
|
+
'createdAt',
|
|
133
|
+
'ref',
|
|
134
|
+
'remote',
|
|
135
|
+
'schemaVersion',
|
|
136
|
+
'scope',
|
|
137
|
+
'slug',
|
|
138
|
+
'status',
|
|
115
139
|
]);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
if (
|
|
141
|
+
!record ||
|
|
142
|
+
typeof record !== 'object' ||
|
|
143
|
+
Array.isArray(record) ||
|
|
144
|
+
Object.keys(record).some((key) => !expectedKeys.has(key)) ||
|
|
145
|
+
record.schemaVersion !== 1 ||
|
|
146
|
+
record.slug !== projectName ||
|
|
147
|
+
record.scope !== 'synced' ||
|
|
148
|
+
record.ref !== `refs/oat/projects/${projectName}` ||
|
|
149
|
+
record.remote !== 'origin' ||
|
|
150
|
+
record.status !== 'complete' ||
|
|
151
|
+
typeof record.createdAt !== 'string' ||
|
|
152
|
+
Number.isNaN(Date.parse(record.createdAt)) ||
|
|
153
|
+
typeof record.completedAt !== 'string' ||
|
|
154
|
+
Number.isNaN(Date.parse(record.completedAt)) ||
|
|
155
|
+
(record.archiveSnapshot !== undefined &&
|
|
156
|
+
(typeof record.archiveSnapshot !== 'string' ||
|
|
157
|
+
record.archiveSnapshot.length === 0))
|
|
158
|
+
) {
|
|
159
|
+
fail(
|
|
160
|
+
'committed record must be a complete synced record with the exact project identity and lifecycle content.',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.stdout.write(`${commit}\n`);
|
|
124
165
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
!
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
record.status !== 'complete' ||
|
|
147
|
-
typeof record.createdAt !== 'string' ||
|
|
148
|
-
Number.isNaN(Date.parse(record.createdAt)) ||
|
|
149
|
-
typeof record.completedAt !== 'string' ||
|
|
150
|
-
Number.isNaN(Date.parse(record.completedAt)) ||
|
|
151
|
-
(record.archiveSnapshot !== undefined &&
|
|
152
|
-
(typeof record.archiveSnapshot !== 'string' ||
|
|
153
|
-
record.archiveSnapshot.length === 0))
|
|
154
|
-
) {
|
|
155
|
-
fail(
|
|
156
|
-
'committed record must be a complete synced record with the exact project identity and lifecycle content.',
|
|
157
|
-
);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
169
|
+
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
170
|
+
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
171
|
+
* and canonicalizing only one side has the same effect under
|
|
172
|
+
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
173
|
+
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
174
|
+
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
175
|
+
* `false`; it never masks a direct run.
|
|
176
|
+
*/
|
|
177
|
+
function isDirectInvocation(invokedPath) {
|
|
178
|
+
if (!invokedPath) return false;
|
|
179
|
+
try {
|
|
180
|
+
return (
|
|
181
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
182
|
+
realpathSync(resolve(invokedPath))
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
158
187
|
}
|
|
159
188
|
|
|
160
|
-
process.
|
|
189
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
190
|
+
main(process.argv.slice(2));
|
|
191
|
+
}
|