@open-agent-toolkit/cli 0.3.16 → 0.3.17
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/agents/oat-phase-implementer.md +26 -6
- package/assets/agents/oat-reviewer.md +51 -1
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/contributing/documentation.md +4 -0
- package/assets/docs/contributing/skills.md +3 -1
- package/assets/docs/getting-started/bootstrap.md +1 -1
- package/assets/docs/getting-started/index.md +0 -4
- package/assets/docs/getting-started/tool-packs.md +0 -7
- package/assets/docs/reference/cli-reference.md +42 -1
- package/assets/docs/reference/config-and-local-state.md +11 -2
- package/assets/docs/reference/project-artifacts.md +43 -8
- package/assets/docs/skills/repo-improve.md +9 -7
- package/assets/docs/workflows/advanced/autonomy.md +40 -0
- package/assets/docs/workflows/backlog-and-planning/backlog-lifecycle.md +25 -24
- package/assets/docs/workflows/backlog-and-planning/remote-project-management.md +0 -7
- package/assets/docs/workflows/ideas/lifecycle.md +2 -7
- package/assets/docs/workflows/projects/reviews/review-flavors.md +8 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-apply/SKILL.md +14 -4
- package/assets/skills/oat-brainstorm/SKILL.md +17 -13
- package/assets/skills/oat-docs-apply/SKILL.md +13 -3
- package/assets/skills/oat-doctor/SKILL.md +11 -1
- package/assets/skills/oat-explainer-kit/SKILL.md +1 -1
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +6 -3
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
- package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +28 -3
- package/assets/skills/oat-project-autonomous/SKILL.md +60 -5
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +44 -6
- package/assets/skills/oat-project-capture/SKILL.md +7 -3
- package/assets/skills/oat-project-complete/SKILL.md +39 -34
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +106 -28
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +30 -3
- package/assets/skills/oat-project-design/SKILL.md +12 -9
- package/assets/skills/oat-project-discover/SKILL.md +8 -4
- package/assets/skills/oat-project-document/SKILL.md +14 -5
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +44 -6
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +6 -6
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +44 -6
- package/assets/skills/oat-project-implement/references/phase-execution.md +20 -6
- package/assets/skills/oat-project-import-plan/SKILL.md +20 -6
- package/assets/skills/oat-project-lite/SKILL.md +7 -5
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +44 -6
- package/assets/skills/oat-project-new/SKILL.md +6 -2
- package/assets/skills/oat-project-plan/SKILL.md +14 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +44 -6
- package/assets/skills/oat-project-progress/SKILL.md +13 -1
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +7 -3
- package/assets/skills/oat-project-quick-start/SKILL.md +17 -12
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +44 -6
- package/assets/skills/oat-project-reconcile/SKILL.md +8 -4
- package/assets/skills/oat-project-retro/SKILL.md +1 -1
- package/assets/skills/oat-project-retro/references/apply-procedure.md +6 -6
- package/assets/skills/oat-project-retro-file/SKILL.md +7 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +23 -4
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +19 -1
- package/assets/skills/oat-project-review-receive/SKILL.md +37 -12
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +7 -3
- package/assets/skills/oat-project-revise/SKILL.md +13 -5
- package/assets/skills/oat-project-spec/SKILL.md +8 -4
- package/assets/skills/oat-project-summary/SKILL.md +11 -7
- package/assets/skills/oat-repo-knowledge-index/SKILL.md +54 -20
- package/assets/skills/oat-repo-knowledge-index/scripts/refresh-owned.mjs +118 -0
- package/assets/skills/oat-review-provide/SKILL.md +7 -3
- package/assets/skills/oat-review-receive-remote/SKILL.md +14 -4
- package/assets/skills/oat-wave-execute/SKILL.md +24 -11
- package/assets/skills/oat-wave-execute/assets/wrapper-plan-template.md +16 -2
- package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +45 -17
- package/assets/templates/pjm-agents.md +38 -8
- package/assets/templates/repo-agents.md +5 -0
- package/assets/templates/repo-readme.md +5 -0
- package/dist/commands/backlog/archive.d.ts +3 -0
- package/dist/commands/backlog/archive.d.ts.map +1 -1
- package/dist/commands/backlog/archive.js +63 -26
- package/dist/commands/backlog/index.d.ts.map +1 -1
- package/dist/commands/backlog/index.js +3 -0
- package/dist/commands/backlog/rewrite-references.d.ts +5 -0
- package/dist/commands/backlog/rewrite-references.d.ts.map +1 -1
- package/dist/commands/backlog/rewrite-references.js +39 -1
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +1 -0
- package/dist/commands/internal/commit-paths.d.ts +3 -0
- package/dist/commands/internal/commit-paths.d.ts.map +1 -0
- package/dist/commands/internal/commit-paths.js +31 -0
- package/dist/commands/internal/index.d.ts.map +1 -1
- package/dist/commands/internal/index.js +2 -0
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +37 -8
- package/dist/commands/project/archive/archive-utils.d.ts +6 -3
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +510 -104
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +1 -0
- package/dist/commands/project/log/append.d.ts +2 -3
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +55 -101
- package/dist/commands/project/migrate/index.d.ts +1 -2
- package/dist/commands/project/migrate/index.d.ts.map +1 -1
- package/dist/commands/project/migrate/index.js +8 -2
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +4 -4
- package/dist/commands/project/promote/promote.d.ts +8 -0
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +37 -6
- package/dist/commands/project/status.d.ts.map +1 -1
- package/dist/commands/project/status.js +3 -0
- package/dist/commands/project/sync/ref-sync.d.ts +12 -1
- package/dist/commands/project/sync/ref-sync.d.ts.map +1 -1
- package/dist/commands/project/sync/ref-sync.js +347 -23
- package/dist/commands/shared/exact-path-commit.d.ts +32 -0
- package/dist/commands/shared/exact-path-commit.d.ts.map +1 -0
- package/dist/commands/shared/exact-path-commit.js +691 -0
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +138 -11
- package/package.json +3 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, link, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
4
|
import { ensureScopedRootGitignore } from '../../init/gitignore.js';
|
|
5
|
+
import { commitExactPaths, } from '../../shared/exact-path-commit.js';
|
|
4
6
|
import { getFrontmatterBlock } from '../../shared/frontmatter.js';
|
|
5
7
|
import { canonicalizePath, completedSyncedRefName, isSyncedCheckout, resolveScopeRoot, SYNCED_REMOTE, syncedRefName, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
6
8
|
import { readOatLocalConfig, writeOatLocalConfig } from '../../../config/oat-config.js';
|
|
@@ -8,7 +10,7 @@ import { CliError } from '../../../errors/cli-error.js';
|
|
|
8
10
|
import { copyDirectory } from '../../../fs/io.js';
|
|
9
11
|
import YAML from 'yaml';
|
|
10
12
|
import { isAtomicPushUnsupported, pushAtomicRefTransition, } from './git.js';
|
|
11
|
-
import { buildSyncedRecord, readSyncedRecord, writeSyncedRecord, } from './record.js';
|
|
13
|
+
import { buildSyncedRecord, readSyncedRecord, SyncedProjectRecordSchema, writeSyncedRecord, } from './record.js';
|
|
12
14
|
export function classifyRemoteRefLookup(result, remote, ref) {
|
|
13
15
|
if (result.code === 0)
|
|
14
16
|
return 'present';
|
|
@@ -593,22 +595,126 @@ export async function abortSynced(target, git) {
|
|
|
593
595
|
});
|
|
594
596
|
}
|
|
595
597
|
function normalizedPathspecs(repoRoot, pathspecs) {
|
|
596
|
-
return
|
|
598
|
+
return [
|
|
599
|
+
...new Set(pathspecs.map((pathspec) => repoRelativePath(repoRoot, pathspec))),
|
|
600
|
+
].sort();
|
|
597
601
|
}
|
|
598
|
-
|
|
602
|
+
class PendingRecordCommitError extends CliError {
|
|
603
|
+
result;
|
|
604
|
+
constructor(message, result) {
|
|
605
|
+
super(message, result.outcome === 'blocked' ? 1 : 2);
|
|
606
|
+
this.result = result;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
export async function commitRecordChange(repoRoot, pathspecs, message, git, options, settlement) {
|
|
599
610
|
assertAllowlistedPathspecs(repoRoot, pathspecs, options);
|
|
600
611
|
const normalized = normalizedPathspecs(repoRoot, pathspecs);
|
|
601
|
-
await git.run(['
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
612
|
+
await git.run(['rev-parse', '--is-inside-work-tree'], { cwd: repoRoot });
|
|
613
|
+
const gitDir = (await git.run(['rev-parse', '--absolute-git-dir'], { cwd: repoRoot })).stdout;
|
|
614
|
+
const pendingDir = join(gitDir, 'oat-record-commit-pending');
|
|
615
|
+
await mkdir(pendingDir, { recursive: true });
|
|
616
|
+
const operation = createHash('sha256')
|
|
617
|
+
.update(JSON.stringify([message, normalized]))
|
|
618
|
+
.digest('hex');
|
|
619
|
+
const pendingPath = join(pendingDir, `${operation}.json`);
|
|
620
|
+
for (let reservationAttempt = 0; reservationAttempt < 2; reservationAttempt++) {
|
|
621
|
+
if ((await readOptionalFile(pendingPath)) === null) {
|
|
622
|
+
if (settlement)
|
|
623
|
+
throw new CliError('The verified pending migration marker disappeared; preserve state for inspection.', 2);
|
|
624
|
+
const identity = `project-record:${operation}:${randomUUID()}`;
|
|
625
|
+
const temporary = await mkdtemp(join(pendingDir, 'reservation-'));
|
|
626
|
+
const complete = join(temporary, 'pending.json');
|
|
627
|
+
try {
|
|
628
|
+
await writeFile(complete, JSON.stringify({ identity, paths: normalized, message }));
|
|
629
|
+
try {
|
|
630
|
+
await link(complete, pendingPath);
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (error.code !== 'EEXIST')
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
finally {
|
|
638
|
+
await rm(temporary, { recursive: true, force: true });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const file = await open(pendingPath, 'r');
|
|
642
|
+
const markerIdentity = await file.stat();
|
|
643
|
+
let markerBytes;
|
|
644
|
+
try {
|
|
645
|
+
markerBytes = await file.readFile('utf8');
|
|
646
|
+
}
|
|
647
|
+
finally {
|
|
648
|
+
await file.close();
|
|
649
|
+
}
|
|
650
|
+
const reservation = JSON.parse(markerBytes);
|
|
651
|
+
if (settlement &&
|
|
652
|
+
(reservation.identity !== settlement.marker.identity ||
|
|
653
|
+
markerBytes !== settlement.marker.bytes ||
|
|
654
|
+
markerIdentity.ino !== settlement.marker.ino ||
|
|
655
|
+
markerIdentity.dev !== settlement.marker.dev))
|
|
656
|
+
throw new CliError('The verified pending migration marker was replaced; replacement preserved for inspection.', 2);
|
|
657
|
+
const clearPending = async () => {
|
|
658
|
+
const current = await lstat(pendingPath).catch((error) => {
|
|
659
|
+
if (error.code === 'ENOENT')
|
|
660
|
+
return undefined;
|
|
661
|
+
throw error;
|
|
662
|
+
});
|
|
663
|
+
if (!current)
|
|
664
|
+
return;
|
|
665
|
+
if (current.ino !== markerIdentity.ino ||
|
|
666
|
+
current.dev !== markerIdentity.dev ||
|
|
667
|
+
(await readOptionalFile(pendingPath)) !== markerBytes)
|
|
668
|
+
throw new CliError(`Verified commit exists, but the pending marker at ${pendingPath} was replaced; replacement preserved for inspection.`, 2);
|
|
669
|
+
await rm(pendingPath);
|
|
670
|
+
};
|
|
671
|
+
if (reservation.message !== message ||
|
|
672
|
+
JSON.stringify(reservation.paths) !== JSON.stringify(normalized) ||
|
|
673
|
+
!reservation.identity.startsWith(`project-record:${operation}:`))
|
|
674
|
+
throw new CliError(`Unreconciled lifecycle commit marker at ${pendingPath}; preserve it and inspect before retrying.`, 2);
|
|
675
|
+
const result = await commitExactPaths({
|
|
676
|
+
repoRoot,
|
|
677
|
+
paths: normalized,
|
|
678
|
+
message,
|
|
679
|
+
identity: reservation.identity,
|
|
680
|
+
});
|
|
681
|
+
// A public helper repair can settle Git while leaving our reservation. Only
|
|
682
|
+
// its positive prior/current publication proof authorizes retiring that
|
|
683
|
+
// reservation before one fresh generation. Never republish a superseded
|
|
684
|
+
// tree; a migration's finalization remains mandatory.
|
|
685
|
+
if (!settlement &&
|
|
686
|
+
reservationAttempt === 0 &&
|
|
687
|
+
result.outcome === 'failed' &&
|
|
688
|
+
result.settledCommit &&
|
|
689
|
+
result.settledCommit === result.commit) {
|
|
690
|
+
await clearPending();
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (settlement &&
|
|
694
|
+
['committed', 'already-matching', 'nothing'].includes(result.outcome) &&
|
|
695
|
+
result.commit !== settlement.commit)
|
|
696
|
+
throw new CliError('Pending migration settlement did not return its verified commit; marker preserved for inspection.', 2);
|
|
697
|
+
if (result.outcome === 'nothing') {
|
|
698
|
+
await clearPending();
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
if ((result.outcome === 'committed' ||
|
|
702
|
+
result.outcome === 'already-matching') &&
|
|
703
|
+
result.commit) {
|
|
704
|
+
try {
|
|
705
|
+
await settlement?.finalize();
|
|
706
|
+
await clearPending();
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
throw new PendingRecordCommitError(errorMessage(error), result);
|
|
710
|
+
}
|
|
711
|
+
return { sha: result.commit };
|
|
712
|
+
}
|
|
713
|
+
throw new PendingRecordCommitError(result.resumable === false
|
|
714
|
+
? `Exact-path lifecycle commit ${result.outcome}: ${result.error ?? 'unverified result'}. Preserve the pending marker at ${pendingPath}${result.receipt ? ` and receipt at ${result.receipt}` : ''} for inspection and explicit provenance reconciliation before any further commit attempt.`
|
|
715
|
+
: `Exact-path lifecycle commit ${result.outcome}: ${result.error ?? 'unverified result'}. Retry after resolving the reported condition with: oat internal commit-paths --identity ${shellQuote(reservation.identity)} --message ${shellQuote(message)} -- ${normalized.map(shellQuote).join(' ')}${result.receipt ? `; receipt ${result.receipt}` : ''}.`, result);
|
|
606
716
|
}
|
|
607
|
-
|
|
608
|
-
cwd: repoRoot,
|
|
609
|
-
});
|
|
610
|
-
const sha = (await git.run(['rev-parse', 'HEAD'], { cwd: repoRoot })).stdout;
|
|
611
|
-
return { sha };
|
|
717
|
+
throw new CliError('Lifecycle commit reservation changed repeatedly; preserve pending state for inspection.', 2);
|
|
612
718
|
}
|
|
613
719
|
export async function preflightSyncedCheckout(target, git) {
|
|
614
720
|
const checkoutExists = await assertCanonicalSyncTargetIdentity(target);
|
|
@@ -888,14 +994,8 @@ export async function pruneSynced(target, git, options) {
|
|
|
888
994
|
if (!options.commit) {
|
|
889
995
|
return { status: 'pruned', lifecycleCommit: null };
|
|
890
996
|
}
|
|
891
|
-
await
|
|
892
|
-
|
|
893
|
-
'-m',
|
|
894
|
-
`chore(oat): prune synced project ${target.slug}`,
|
|
895
|
-
'--',
|
|
896
|
-
relativeRecordPath,
|
|
897
|
-
], { cwd: target.repoRoot });
|
|
898
|
-
const lifecycleCommit = (await git.run(['rev-parse', 'HEAD'], { cwd: target.repoRoot })).stdout;
|
|
997
|
+
const committed = await commitRecordChange(target.repoRoot, [relativeRecordPath], `chore(oat): prune synced project ${target.slug}`, git, { projectRoots: target });
|
|
998
|
+
const lifecycleCommit = committed?.sha ?? null;
|
|
899
999
|
return { status: 'pruned', lifecycleCommit };
|
|
900
1000
|
}
|
|
901
1001
|
}
|
|
@@ -990,10 +1090,206 @@ async function readOptionalFile(path) {
|
|
|
990
1090
|
throw error;
|
|
991
1091
|
}
|
|
992
1092
|
}
|
|
1093
|
+
// Only the existing migration reservation can resume after its source was removed.
|
|
1094
|
+
// No ownership is inferred from the current staged index or a missing directory.
|
|
1095
|
+
async function verifyPendingMigration(target, git, options, sourcePath) {
|
|
1096
|
+
const stop = () => {
|
|
1097
|
+
throw new CliError('Pending migration does not positively match its retained operation, parent source tree and published synced checkout; preserve all state for inspection.', 2);
|
|
1098
|
+
};
|
|
1099
|
+
const sourceRelative = repoRelativePath(target.repoRoot, sourcePath);
|
|
1100
|
+
if (!options.commit ||
|
|
1101
|
+
canonicalizePath(sourcePath) !==
|
|
1102
|
+
resolve(canonicalizePath(target.sharedRoot), target.slug))
|
|
1103
|
+
stop();
|
|
1104
|
+
if (await lstat(sourcePath).then(() => true, (error) => {
|
|
1105
|
+
if (error.code === 'ENOENT')
|
|
1106
|
+
return false;
|
|
1107
|
+
throw error;
|
|
1108
|
+
}))
|
|
1109
|
+
stop();
|
|
1110
|
+
const gitDir = (await git.run(['rev-parse', '--absolute-git-dir'], { cwd: target.repoRoot })).stdout;
|
|
1111
|
+
const pendingDir = join(gitDir, 'oat-record-commit-pending');
|
|
1112
|
+
const message = `chore(oat): migrate ${target.slug} to synced scope`;
|
|
1113
|
+
const candidates = [];
|
|
1114
|
+
for (const name of await readdir(pendingDir).catch((error) => {
|
|
1115
|
+
if (error.code === 'ENOENT')
|
|
1116
|
+
return [];
|
|
1117
|
+
throw error;
|
|
1118
|
+
})) {
|
|
1119
|
+
if (!name.endsWith('.json'))
|
|
1120
|
+
continue;
|
|
1121
|
+
const path = join(pendingDir, name);
|
|
1122
|
+
const file = await open(path, 'r');
|
|
1123
|
+
let bytes;
|
|
1124
|
+
let identity;
|
|
1125
|
+
try {
|
|
1126
|
+
identity = await file.stat();
|
|
1127
|
+
bytes = await file.readFile('utf8');
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
await file.close();
|
|
1131
|
+
}
|
|
1132
|
+
const marker = JSON.parse(bytes);
|
|
1133
|
+
if (marker.message === message)
|
|
1134
|
+
candidates.push({ name, path, bytes, marker, identity });
|
|
1135
|
+
}
|
|
1136
|
+
if (candidates.length !== 1)
|
|
1137
|
+
stop();
|
|
1138
|
+
const { name, path, bytes, marker, identity } = candidates[0];
|
|
1139
|
+
if (!Array.isArray(marker.paths) ||
|
|
1140
|
+
!marker.paths.every((value) => typeof value === 'string') ||
|
|
1141
|
+
typeof marker.identity !== 'string')
|
|
1142
|
+
stop();
|
|
1143
|
+
const paths = normalizedPathspecs(target.repoRoot, marker.paths);
|
|
1144
|
+
const operation = createHash('sha256')
|
|
1145
|
+
.update(JSON.stringify([message, paths]))
|
|
1146
|
+
.digest('hex');
|
|
1147
|
+
const recordPath = syncedRecordPath(target.syncedRoot, target.slug);
|
|
1148
|
+
const recordRelative = repoRelativePath(target.repoRoot, recordPath);
|
|
1149
|
+
if (name !== `${operation}.json` ||
|
|
1150
|
+
!marker.identity.startsWith(`project-record:${operation}:`) ||
|
|
1151
|
+
JSON.stringify(paths) !== JSON.stringify(marker.paths) ||
|
|
1152
|
+
!paths.includes(recordRelative) ||
|
|
1153
|
+
paths.some((value) => value !== recordRelative &&
|
|
1154
|
+
value !== '.gitignore' &&
|
|
1155
|
+
!value.startsWith(`${sourceRelative}/`)))
|
|
1156
|
+
stop();
|
|
1157
|
+
const receiptPath = join(gitDir, 'oat-exact-path-commits', `${createHash('sha256').update(marker.identity).digest('hex')}.json`);
|
|
1158
|
+
const receiptBytes = await readFile(receiptPath, 'utf8');
|
|
1159
|
+
const receipt = JSON.parse(receiptBytes);
|
|
1160
|
+
if (!receipt.commit ||
|
|
1161
|
+
!receipt.tree ||
|
|
1162
|
+
receipt.identity !== marker.identity ||
|
|
1163
|
+
JSON.stringify(receipt.paths) !== JSON.stringify(paths))
|
|
1164
|
+
stop();
|
|
1165
|
+
const commit = receipt.commit;
|
|
1166
|
+
if (!commit)
|
|
1167
|
+
return stop();
|
|
1168
|
+
const run = async (args, cwd = target.repoRoot) => (await git.run(['--literal-pathspecs', ...args], { cwd })).stdout;
|
|
1169
|
+
if ((await run(['rev-parse', 'HEAD'])) !== commit ||
|
|
1170
|
+
(await run(['show', '-s', '--format=%P', commit])) !== receipt.parent ||
|
|
1171
|
+
(await run(['rev-parse', `${commit}^{tree}`])) !== receipt.tree ||
|
|
1172
|
+
!(await run(['show', '-s', '--format=%B', commit])).includes(`Oat-Operation: ${createHash('sha256').update(marker.identity).digest('hex')}`))
|
|
1173
|
+
stop();
|
|
1174
|
+
const removed = (await run([
|
|
1175
|
+
'ls-tree',
|
|
1176
|
+
'-r',
|
|
1177
|
+
'--name-only',
|
|
1178
|
+
'-z',
|
|
1179
|
+
receipt.parent,
|
|
1180
|
+
'--',
|
|
1181
|
+
sourceRelative,
|
|
1182
|
+
]))
|
|
1183
|
+
.split('\0')
|
|
1184
|
+
.filter(Boolean)
|
|
1185
|
+
.sort();
|
|
1186
|
+
if (!removed.length ||
|
|
1187
|
+
JSON.stringify(removed) !==
|
|
1188
|
+
JSON.stringify(paths.filter((value) => value.startsWith(`${sourceRelative}/`))))
|
|
1189
|
+
stop();
|
|
1190
|
+
if ((await run([
|
|
1191
|
+
'ls-tree',
|
|
1192
|
+
'-r',
|
|
1193
|
+
'--name-only',
|
|
1194
|
+
commit,
|
|
1195
|
+
'--',
|
|
1196
|
+
sourceRelative,
|
|
1197
|
+
])) !== '')
|
|
1198
|
+
stop();
|
|
1199
|
+
const emitted = (await run([
|
|
1200
|
+
'diff-tree',
|
|
1201
|
+
'--root',
|
|
1202
|
+
'--no-commit-id',
|
|
1203
|
+
'-r',
|
|
1204
|
+
'--no-renames',
|
|
1205
|
+
'--name-only',
|
|
1206
|
+
'-z',
|
|
1207
|
+
commit,
|
|
1208
|
+
]))
|
|
1209
|
+
.split('\0')
|
|
1210
|
+
.filter(Boolean);
|
|
1211
|
+
if (emitted.some((value) => !paths.includes(value)))
|
|
1212
|
+
stop();
|
|
1213
|
+
const committedRecord = SyncedProjectRecordSchema.parse(JSON.parse(await run(['show', `${commit}:${recordRelative}`])));
|
|
1214
|
+
const record = await readSyncedRecord(recordPath);
|
|
1215
|
+
if (!record ||
|
|
1216
|
+
record.slug !== target.slug ||
|
|
1217
|
+
record.ref !== target.ref ||
|
|
1218
|
+
record.remote !== target.remote ||
|
|
1219
|
+
record.status !== 'active' ||
|
|
1220
|
+
JSON.stringify(record) !== JSON.stringify(committedRecord))
|
|
1221
|
+
stop();
|
|
1222
|
+
if (!(await assertCanonicalSyncTargetIdentity(target)))
|
|
1223
|
+
return stop();
|
|
1224
|
+
await assertNestedWorktree(target, git);
|
|
1225
|
+
const sha = await headSha(target, git);
|
|
1226
|
+
const remote = await git.run(['ls-remote', '--exit-code', target.remote, target.ref], { cwd: target.repoRoot, allowFailure: true });
|
|
1227
|
+
if (remote.code !== 0 ||
|
|
1228
|
+
remote.stdout.split(/\s+/)[0] !== sha ||
|
|
1229
|
+
(await run(['status', '--porcelain'], target.projectPath)) !== '' ||
|
|
1230
|
+
(await run(['rev-parse', `${receipt.parent}:${sourceRelative}`])) !==
|
|
1231
|
+
(await run(['rev-parse', 'HEAD^{tree}'], target.projectPath)))
|
|
1232
|
+
return stop();
|
|
1233
|
+
const currentMarker = await lstat(path);
|
|
1234
|
+
if (currentMarker.ino !== identity.ino ||
|
|
1235
|
+
currentMarker.dev !== identity.dev ||
|
|
1236
|
+
(await readFile(path, 'utf8')) !== bytes ||
|
|
1237
|
+
(await readFile(receiptPath, 'utf8')) !== receiptBytes)
|
|
1238
|
+
stop();
|
|
1239
|
+
return {
|
|
1240
|
+
commit,
|
|
1241
|
+
parent: receipt.parent,
|
|
1242
|
+
paths,
|
|
1243
|
+
message,
|
|
1244
|
+
sha,
|
|
1245
|
+
sourceRelative,
|
|
1246
|
+
marker: {
|
|
1247
|
+
identity: marker.identity,
|
|
1248
|
+
bytes,
|
|
1249
|
+
ino: identity.ino,
|
|
1250
|
+
dev: identity.dev,
|
|
1251
|
+
},
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
async function finalizePendingMigration(target, git, options, sourcePath) {
|
|
1255
|
+
const { commit, paths, message, sha, sourceRelative, marker } = await verifyPendingMigration(target, git, options, sourcePath);
|
|
1256
|
+
const committed = await commitRecordChange(target.repoRoot, paths, message, git, { projectRoots: target }, {
|
|
1257
|
+
commit,
|
|
1258
|
+
marker,
|
|
1259
|
+
finalize: async () => {
|
|
1260
|
+
const local = await (options.readOatLocalConfig ?? readOatLocalConfig)(target.repoRoot);
|
|
1261
|
+
if (local.activeProject === sourceRelative)
|
|
1262
|
+
await (options.writeOatLocalConfig ?? writeOatLocalConfig)(target.repoRoot, {
|
|
1263
|
+
...local,
|
|
1264
|
+
activeProject: repoRelativePath(target.repoRoot, target.projectPath),
|
|
1265
|
+
});
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
if (committed?.sha !== commit)
|
|
1269
|
+
throw new CliError('Pending migration settlement did not return its verified commit; preserve state for inspection.', 2);
|
|
1270
|
+
return {
|
|
1271
|
+
status: 'migrated',
|
|
1272
|
+
lifecycleCommit: commit,
|
|
1273
|
+
sha,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
993
1276
|
export async function migrateSharedToSynced(target, git, options) {
|
|
994
1277
|
const sourcePath = resolve(options.sourcePath);
|
|
1278
|
+
const sourceExists = await lstat(sourcePath).catch((error) => {
|
|
1279
|
+
if (error.code === 'ENOENT')
|
|
1280
|
+
return undefined;
|
|
1281
|
+
throw error;
|
|
1282
|
+
});
|
|
1283
|
+
if (!sourceExists) {
|
|
1284
|
+
if ((await readSyncedRecord(syncedRecordPath(target.syncedRoot, target.slug))) === null)
|
|
1285
|
+
await assertConfinedMigrationSource(target, sourcePath);
|
|
1286
|
+
return finalizePendingMigration(target, git, options, sourcePath);
|
|
1287
|
+
}
|
|
995
1288
|
await assertConfinedMigrationSource(target, sourcePath);
|
|
996
1289
|
const sourceRelative = repoRelativePath(target.repoRoot, sourcePath);
|
|
1290
|
+
const sourceFiles = (await git.run(['--literal-pathspecs', 'ls-files', '-z', '--', sourceRelative], { cwd: target.repoRoot })).stdout
|
|
1291
|
+
.split('\0')
|
|
1292
|
+
.filter(Boolean);
|
|
997
1293
|
const recordPath = syncedRecordPath(target.syncedRoot, target.slug);
|
|
998
1294
|
const destinationRelative = repoRelativePath(target.repoRoot, target.projectPath);
|
|
999
1295
|
const gitignorePath = join(target.repoRoot, '.gitignore');
|
|
@@ -1030,6 +1326,7 @@ export async function migrateSharedToSynced(target, git, options) {
|
|
|
1030
1326
|
let prePublishRemoteSha = null;
|
|
1031
1327
|
let activeProjectUpdated = false;
|
|
1032
1328
|
let gitignoreSelfHealStarted = false;
|
|
1329
|
+
let migrationCommitPaths;
|
|
1033
1330
|
try {
|
|
1034
1331
|
if (needsGitignoreHeal) {
|
|
1035
1332
|
const repair = await (options.ensureScopedRootGitignore ?? ensureScopedRootGitignore)(target.repoRoot, target.syncedRoot, 'synced', git, 'Cannot add the synced-project rule because .gitignore has staged or unstaged changes; commit or stash those changes, or add the managed rule manually, then retry migration.');
|
|
@@ -1054,7 +1351,12 @@ export async function migrateSharedToSynced(target, git, options) {
|
|
|
1054
1351
|
(await readOptionalFile(gitignorePath)) !== originalGitignore;
|
|
1055
1352
|
let lifecycleCommit = null;
|
|
1056
1353
|
if (options.commit) {
|
|
1057
|
-
|
|
1354
|
+
migrationCommitPaths = [
|
|
1355
|
+
...sourceFiles,
|
|
1356
|
+
recordPath,
|
|
1357
|
+
...(gitignoreChanged ? [gitignorePath] : []),
|
|
1358
|
+
];
|
|
1359
|
+
const committed = await commitRecordChange(target.repoRoot, migrationCommitPaths, `chore(oat): migrate ${target.slug} to synced scope`, git, { projectRoots: target });
|
|
1058
1360
|
lifecycleCommit = committed?.sha ?? null;
|
|
1059
1361
|
await options.afterBranchCommit?.();
|
|
1060
1362
|
}
|
|
@@ -1078,6 +1380,28 @@ export async function migrateSharedToSynced(target, git, options) {
|
|
|
1078
1380
|
return { status: 'migrated', lifecycleCommit, sha: pushed.sha };
|
|
1079
1381
|
}
|
|
1080
1382
|
catch (error) {
|
|
1383
|
+
if (error instanceof PendingRecordCommitError && error.result.commit) {
|
|
1384
|
+
// committed:true is not a provenance proof: the helper can report a
|
|
1385
|
+
// stored receipt's commit before validating it. Reuse the owning verifier
|
|
1386
|
+
// and bind its proof to this invocation before choosing retention.
|
|
1387
|
+
let retained;
|
|
1388
|
+
try {
|
|
1389
|
+
retained = await verifyPendingMigration(target, git, options, sourcePath);
|
|
1390
|
+
if (retained.commit !== error.result.commit ||
|
|
1391
|
+
retained.parent !== preMigrationHead ||
|
|
1392
|
+
!migrationCommitPaths ||
|
|
1393
|
+
JSON.stringify(retained.paths) !==
|
|
1394
|
+
JSON.stringify(normalizedPathspecs(target.repoRoot, migrationCommitPaths)))
|
|
1395
|
+
throw new Error('Retained operation does not match this migration invocation.', { cause: error });
|
|
1396
|
+
}
|
|
1397
|
+
catch (verificationError) {
|
|
1398
|
+
throw new CliError(`${error.message} Migration commit provenance could not be independently verified; no rollback or finalization was attempted. Preserve all current state and recovery metadata for inspection. ${errorMessage(verificationError)}`, 2);
|
|
1399
|
+
}
|
|
1400
|
+
// The verifier proved the removed source is absent. Restore only retained
|
|
1401
|
+
// tree files; the owning finalizer will settle source deletions in the index.
|
|
1402
|
+
const restorePaths = retained.paths.filter((value) => !value.startsWith(`${sourceRelative}/`));
|
|
1403
|
+
throw new CliError(`${error.message} The verified migration commit and published synced checkout were retained. Inspect the hook's changes; removing the hook does not repair its changed bytes. After inspection, restore owned worktree bytes with: git --literal-pathspecs -C ${shellQuote(target.repoRoot)} restore --source ${shellQuote(retained.commit)} --worktree -- ${restorePaths.map(shellQuote).join(' ')}; then finalize with: oat --cwd ${shellQuote(target.repoRoot)} project migrate ${shellQuote(sourceRelative)} --to synced`, error.exitCode);
|
|
1404
|
+
}
|
|
1081
1405
|
const compensationFailures = [];
|
|
1082
1406
|
const compensate = async (resource, recovery, action) => {
|
|
1083
1407
|
try {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ExactPathCommitInput {
|
|
2
|
+
repoRoot: string;
|
|
3
|
+
paths: readonly string[];
|
|
4
|
+
message: string;
|
|
5
|
+
identity: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ExactPathCommitResult {
|
|
8
|
+
outcome: 'committed' | 'already-matching' | 'nothing' | 'blocked' | 'failed';
|
|
9
|
+
committed: boolean;
|
|
10
|
+
attempts: number;
|
|
11
|
+
commit?: string;
|
|
12
|
+
/** A fully verified resumed receipt whose owned real-index entries remain
|
|
13
|
+
* published, or whose later generation is published at current HEAD. Failed
|
|
14
|
+
* outcome stays failed; only the marker owner may retire that reservation. */
|
|
15
|
+
settledCommit?: string;
|
|
16
|
+
receipt?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
lockClass?: 'transient-index-lock' | 'persistent-index-lock' | 'other';
|
|
19
|
+
resumable?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ExactPathCommitDependencies {
|
|
22
|
+
attempts: number;
|
|
23
|
+
retryDelaysMs: readonly number[];
|
|
24
|
+
sleep: (ms: number) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/** Use an isolated index for enabled hooks; publish only owned entries under Git's lock.
|
|
27
|
+
* The real lock is held during hooks, so ordinary competing Git writers cannot lose
|
|
28
|
+
* their changes. A byte comparison also detects writers bypassing that protocol.
|
|
29
|
+
* Never restore a snapshot over current user data, and never remove a foreign lock.
|
|
30
|
+
*/
|
|
31
|
+
export declare function commitExactPaths(input: ExactPathCommitInput, overrides?: Partial<ExactPathCommitDependencies>): Promise<ExactPathCommitResult>;
|
|
32
|
+
//# sourceMappingURL=exact-path-commit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exact-path-commit.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/exact-path-commit.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,WAAW,GAAG,kBAAkB,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC7E,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;kFAE8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,sBAAsB,GAAG,uBAAuB,GAAG,OAAO,CAAC;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AACD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAuSD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,oBAAoB,EAC3B,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAAC,qBAAqB,CAAC,CA8iBhC"}
|