forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Pre-commit auto-sync for the committed `.agents/skills` mirror.
|
|
6
|
-
*
|
|
7
|
-
* `.agents/skills` is Codex's repo-local discovery path and the ONE skill mirror
|
|
8
|
-
* committed to the repo (so a teammate who clones WITHOUT running `forge setup`
|
|
9
|
-
* still gets Forge skills/stages auto-discovered). It must stay byte-identical to
|
|
10
|
-
* the canonical `skills/` source, which the structural drift gate enforces.
|
|
11
|
-
*
|
|
12
|
-
* This hook removes the regen friction: whenever `skills/**` is staged, it
|
|
13
|
-
* regenerates `.agents/skills` from `skills/` (reusing the same
|
|
14
|
-
* `populateCodexRepoSkills` / `populateAgentSkills` sync the drift gate checks
|
|
15
|
-
* against) and re-stages it, so the committed mirror never drifts from canonical.
|
|
16
|
-
*
|
|
17
|
-
* It is intentionally scoped to `skills/**` via lefthook's `glob`, so it only runs
|
|
18
|
-
* when the canonical source actually changes.
|
|
19
|
-
*/
|
|
4
|
+
/** Command-owned pre-commit sync for the committed `.agents/skills` mirror. */
|
|
20
5
|
|
|
6
|
+
const fs = require('node:fs');
|
|
21
7
|
const path = require('node:path');
|
|
22
8
|
const { execFileSync } = require('node:child_process');
|
|
23
9
|
const { populateCodexRepoSkills, resolveCodexRepoSkillsDir } = require('../lib/codex-skills');
|
|
10
|
+
const { gitIgnoredCanonicalPaths, isValidSkillName, listCanonicalSkills } = require('../lib/skills-sync');
|
|
11
|
+
const {
|
|
12
|
+
completeGeneratedHarnessSkillAuthorization,
|
|
13
|
+
issueGeneratedHarnessSkillAuthorization,
|
|
14
|
+
} = require('../lib/protected-state-authority');
|
|
15
|
+
|
|
16
|
+
function strictIgnoredCanonicalPaths(root, runGit) {
|
|
17
|
+
return gitIgnoredCanonicalPaths(root, runGit, { strict: true });
|
|
18
|
+
}
|
|
24
19
|
|
|
25
20
|
function repoRoot() {
|
|
26
21
|
try {
|
|
@@ -33,30 +28,334 @@ function repoRoot() {
|
|
|
33
28
|
}
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
function actorFromEnv(env) {
|
|
32
|
+
return env.FORGE_ACTOR || env.USER || env.USERNAME || 'unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseGitPaths(output) {
|
|
36
|
+
return output.split('\0').filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toRepoPath(value) {
|
|
40
|
+
return path.sep === '\\' ? value.replace(/\\/g, '/') : value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generatedDriftPaths(root, runGit) {
|
|
44
|
+
const commands = [
|
|
45
|
+
['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMRDT', '--', '.agents/skills'],
|
|
46
|
+
['diff', '--name-only', '-z', '--diff-filter=ACMRDT', '--', '.agents/skills'],
|
|
47
|
+
['ls-files', '-z', '--others', '--exclude-standard', '--', '.agents/skills'],
|
|
48
|
+
];
|
|
49
|
+
return new Set(commands.flatMap(args => parseGitPaths(runGit('git', args, {
|
|
50
|
+
cwd: root,
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
}))));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function gitObjectHash(root, args, content, runGit) {
|
|
57
|
+
const hash = runGit('git', ['hash-object', ...args, '--stdin'], {
|
|
58
|
+
cwd: root,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
input: content,
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
}).trim();
|
|
63
|
+
if (!/^[0-9a-f]{40,64}$/.test(hash)) throw new Error('Git returned an invalid object hash while inspecting canonical skill parity');
|
|
64
|
+
return hash;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function worktreeMatchesIndex(root, repoPath, indexedContent, worktreeContent, runGit) {
|
|
68
|
+
return gitObjectHash(root, [], indexedContent, runGit)
|
|
69
|
+
=== gitObjectHash(root, [`--path=${repoPath}`], worktreeContent, runGit);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizedRepoPath(repoPath) {
|
|
73
|
+
return repoPath.normalize('NFC');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function recordNormalizedPath(paths, repoPath) {
|
|
77
|
+
const key = normalizedRepoPath(repoPath);
|
|
78
|
+
const existing = paths.get(key);
|
|
79
|
+
if (existing && existing !== repoPath) throw new Error(`Unicode-equivalent canonical skill paths are ambiguous: ${existing}, ${repoPath}`);
|
|
80
|
+
paths.set(key, repoPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ambiguousCanonicalPaths(root, runGit) {
|
|
84
|
+
const indexedEntries = parseGitPaths(runGit('git', ['ls-files', '-t', '-z', '--', 'skills'], {
|
|
85
|
+
cwd: root,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
}));
|
|
89
|
+
const mirrorSkipWorktree = new Set(parseGitPaths(runGit('git', ['ls-files', '-t', '-z', '--', '.agents/skills'], {
|
|
90
|
+
cwd: root,
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
})).filter(entry => entry.startsWith('S ')).map(entry => normalizedRepoPath(entry.slice(2))));
|
|
94
|
+
const ignoredWorktree = strictIgnoredCanonicalPaths(root, runGit);
|
|
95
|
+
const filesystemPrefixes = listCanonicalSkills(root).map(({ name }) => `skills/${name}/`);
|
|
96
|
+
const canonicalPrefixKeys = new Set(filesystemPrefixes.map(normalizedRepoPath));
|
|
97
|
+
for (const entry of indexedEntries) {
|
|
98
|
+
const match = /^skills\/([^/]+)\/SKILL\.md$/.exec(entry.slice(2));
|
|
99
|
+
if (match && isValidSkillName(match[1])) canonicalPrefixKeys.add(normalizedRepoPath(`skills/${match[1]}/`));
|
|
100
|
+
}
|
|
101
|
+
const participatesInMirror = repoPath => [...canonicalPrefixKeys].some(prefix => normalizedRepoPath(repoPath).startsWith(prefix));
|
|
102
|
+
const indexed = new Map();
|
|
103
|
+
for (const repoPath of indexedEntries.map(entry => entry.slice(2)).filter(participatesInMirror)) recordNormalizedPath(indexed, repoPath);
|
|
104
|
+
const skipWorktree = new Map();
|
|
105
|
+
for (const repoPath of indexedEntries
|
|
106
|
+
.filter(entry => entry.startsWith('S '))
|
|
107
|
+
.map(entry => entry.slice(2))
|
|
108
|
+
.filter(participatesInMirror)) recordNormalizedPath(skipWorktree, repoPath);
|
|
109
|
+
const worktree = new Map();
|
|
110
|
+
for (const prefix of filesystemPrefixes) {
|
|
111
|
+
const sourceRoot = path.join(root, prefix);
|
|
112
|
+
walkRegularFiles(
|
|
113
|
+
sourceRoot,
|
|
114
|
+
(_file, relative) => recordNormalizedPath(worktree, `${prefix}${relative}`),
|
|
115
|
+
sourceRoot,
|
|
116
|
+
(relative, entry) => entry.isSymbolicLink()
|
|
117
|
+
&& ignoredWorktree.has(normalizedRepoPath(`${prefix}${relative}`)),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
for (const key of ignoredWorktree) {
|
|
121
|
+
if (!indexed.has(key)) worktree.delete(key);
|
|
45
122
|
}
|
|
123
|
+
for (const repoPath of skipWorktree.values()) {
|
|
124
|
+
const mirrorPath = `.agents/${repoPath}`;
|
|
125
|
+
if (!fs.existsSync(path.join(root, repoPath))
|
|
126
|
+
&& (!mirrorSkipWorktree.has(normalizedRepoPath(mirrorPath)) || fs.existsSync(path.join(root, mirrorPath)))) {
|
|
127
|
+
throw new Error(`asymmetric sparse checkout exposes mirror without canonical: ${repoPath}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const paths = new Set([...indexed.keys(), ...worktree.keys()]);
|
|
131
|
+
return new Set([...paths].filter(key => {
|
|
132
|
+
const indexedPath = indexed.get(key);
|
|
133
|
+
const worktreePath = worktree.get(key);
|
|
134
|
+
const indexedContent = indexedPath ? indexedFile(root, indexedPath, runGit) : null;
|
|
135
|
+
let worktreeContent = null;
|
|
136
|
+
try {
|
|
137
|
+
if (worktreePath) worktreeContent = fs.readFileSync(path.join(root, worktreePath));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.code !== 'ENOENT') throw error;
|
|
140
|
+
}
|
|
141
|
+
return indexedContent === null
|
|
142
|
+
? worktreeContent !== null
|
|
143
|
+
: (worktreeContent === null && !skipWorktree.has(key))
|
|
144
|
+
|| (worktreeContent !== null && !worktreeMatchesIndex(root, indexedPath, indexedContent, worktreeContent, runGit));
|
|
145
|
+
}).map(key => indexed.get(key) || worktree.get(key)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function indexedFile(root, repoPath, runGit) {
|
|
149
|
+
const tracked = parseGitPaths(runGit('git', ['ls-files', '-z', '--', `:(literal)${repoPath}`], {
|
|
150
|
+
cwd: root,
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
}));
|
|
154
|
+
const trackedPath = tracked.find(candidate => normalizedRepoPath(candidate) === normalizedRepoPath(repoPath));
|
|
155
|
+
if (!trackedPath) return null;
|
|
156
|
+
return runGit('git', ['show', `:${trackedPath}`], {
|
|
157
|
+
cwd: root,
|
|
158
|
+
encoding: null,
|
|
159
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function walkRegularFiles(root, visit, current = root, shouldSkip = () => false) {
|
|
164
|
+
if (!fs.existsSync(current)) return;
|
|
165
|
+
const currentStat = fs.lstatSync(current);
|
|
166
|
+
if (!currentStat.isDirectory() || currentStat.isSymbolicLink()) throw new Error(`skill sync path is not a real directory: ${current}`);
|
|
167
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
168
|
+
const entryPath = path.join(current, entry.name);
|
|
169
|
+
const relative = toRepoPath(path.relative(root, entryPath));
|
|
170
|
+
if (shouldSkip(relative, entry)) continue;
|
|
171
|
+
const stat = fs.lstatSync(entryPath);
|
|
172
|
+
if (stat.isSymbolicLink()) throw new Error(`skill sync refuses symlink: ${entryPath}`);
|
|
173
|
+
if (stat.isDirectory()) walkRegularFiles(root, visit, entryPath, shouldSkip);
|
|
174
|
+
else if (stat.isFile()) visit(entryPath, relative);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
46
177
|
|
|
178
|
+
function rejectIgnoredSkillDescriptors(root, ignoredCanonical) {
|
|
179
|
+
for (const { name } of listCanonicalSkills(root)) {
|
|
180
|
+
const descriptorRepoPath = `skills/${name}/SKILL.md`;
|
|
181
|
+
if (ignoredCanonical.has(normalizedRepoPath(descriptorRepoPath))) {
|
|
182
|
+
throw new Error(`ignored canonical skill descriptor: ${descriptorRepoPath}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function changedSkillFiles(root, runGit = execFileSync, ignoredCanonical = strictIgnoredCanonicalPaths(root, runGit)) {
|
|
47
188
|
const mirror = resolveCodexRepoSkillsDir(root);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
189
|
+
const drift = generatedDriftPaths(root, runGit);
|
|
190
|
+
const changed = [];
|
|
191
|
+
rejectIgnoredSkillDescriptors(root, ignoredCanonical);
|
|
192
|
+
for (const { name, sourcePath: sourceRoot } of listCanonicalSkills(root)) {
|
|
193
|
+
walkRegularFiles(sourceRoot, (sourcePath, relative) => {
|
|
194
|
+
const canonicalRepoPath = `skills/${name}/${relative}`;
|
|
195
|
+
if (ignoredCanonical.has(normalizedRepoPath(canonicalRepoPath))) {
|
|
196
|
+
const mirrorRepoPath = `.agents/${canonicalRepoPath}`;
|
|
197
|
+
if (indexedFile(root, mirrorRepoPath, runGit) !== null || fs.existsSync(path.join(root, mirrorRepoPath))) {
|
|
198
|
+
throw new Error(`ignored canonical skill path still has a mirror: ${canonicalRepoPath}`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const canonical = fs.readFileSync(sourcePath);
|
|
203
|
+
const mirrorPath = path.join(mirror, name, relative);
|
|
204
|
+
const repoPath = `.agents/skills/${name}/${relative}`;
|
|
205
|
+
const indexedCanonical = indexedFile(root, canonicalRepoPath, runGit);
|
|
206
|
+
if (indexedCanonical !== null
|
|
207
|
+
&& gitObjectHash(root, [], indexedCanonical, runGit)
|
|
208
|
+
!== gitObjectHash(root, [`--path=${repoPath}`], canonical, runGit)) {
|
|
209
|
+
throw new Error(`canonical and generated mirror clean filters differ: ${canonicalRepoPath}`);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
if (canonical.equals(fs.readFileSync(mirrorPath)) && !drift.has(repoPath)) return;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error.code !== 'ENOENT') throw error;
|
|
215
|
+
}
|
|
216
|
+
changed.push({ path: repoPath, canonical, writeIntent: 'update' });
|
|
217
|
+
}, sourceRoot, (relative, entry) => entry.isSymbolicLink()
|
|
218
|
+
&& ignoredCanonical.has(normalizedRepoPath(`skills/${name}/${relative}`)));
|
|
219
|
+
}
|
|
220
|
+
walkRegularFiles(mirror, (mirrorPath, relative) => {
|
|
221
|
+
const canonicalPath = path.join(root, 'skills', relative);
|
|
222
|
+
if (!fs.existsSync(canonicalPath)) {
|
|
223
|
+
changed.push({
|
|
224
|
+
path: `.agents/skills/${relative}`,
|
|
225
|
+
canonical: fs.readFileSync(mirrorPath),
|
|
226
|
+
writeIntent: 'delete',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
const seen = new Set(changed.map(file => file.path));
|
|
231
|
+
for (const repoPath of drift) {
|
|
232
|
+
if (!repoPath.startsWith('.agents/skills/') || seen.has(repoPath)) continue;
|
|
233
|
+
const relative = repoPath.slice('.agents/skills/'.length);
|
|
234
|
+
const canonicalPath = path.join(root, 'skills', relative);
|
|
235
|
+
const mirrorPath = path.join(root, repoPath);
|
|
236
|
+
if (fs.existsSync(canonicalPath) || fs.existsSync(mirrorPath)) continue;
|
|
237
|
+
let priorContent;
|
|
238
|
+
try {
|
|
239
|
+
priorContent = runGit('git', ['show', `HEAD:${repoPath}`], {
|
|
240
|
+
cwd: root,
|
|
241
|
+
encoding: null,
|
|
242
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
243
|
+
});
|
|
244
|
+
} catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
changed.push({ path: repoPath, canonical: priorContent, writeIntent: 'delete' });
|
|
248
|
+
}
|
|
249
|
+
return changed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function sameSkillChanges(expected, actual) {
|
|
253
|
+
if (expected.length !== actual.length) return false;
|
|
254
|
+
const byPath = new Map(actual.map(file => [normalizedRepoPath(file.path), file]));
|
|
255
|
+
if (byPath.size !== actual.length) return false;
|
|
256
|
+
return expected.every(file => {
|
|
257
|
+
const candidate = byPath.get(normalizedRepoPath(file.path));
|
|
258
|
+
return candidate
|
|
259
|
+
&& candidate.writeIntent === file.writeIntent
|
|
260
|
+
&& candidate.canonical.equals(file.canonical);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function syncAgentSkills(options = {}) {
|
|
265
|
+
const root = options.root || repoRoot();
|
|
266
|
+
const env = options.env || process.env;
|
|
267
|
+
const runGit = options.execFileSync || execFileSync;
|
|
268
|
+
const issueAuthorization = options.issueAuthorization || issueGeneratedHarnessSkillAuthorization;
|
|
269
|
+
const completeAuthorization = options.completeAuthorization || completeGeneratedHarnessSkillAuthorization;
|
|
270
|
+
const actor = actorFromEnv(env);
|
|
271
|
+
const sourceHead = runGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
272
|
+
cwd: root,
|
|
273
|
+
encoding: 'utf8',
|
|
274
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
275
|
+
}).trim();
|
|
276
|
+
if (!/^[0-9a-f]{40}$/.test(sourceHead)) throw new Error('source HEAD is not a full lowercase commit SHA');
|
|
277
|
+
|
|
278
|
+
const ignoredCanonical = strictIgnoredCanonicalPaths(root, runGit);
|
|
279
|
+
rejectIgnoredSkillDescriptors(root, ignoredCanonical);
|
|
280
|
+
const ambiguousCanonical = ambiguousCanonicalPaths(root, runGit);
|
|
281
|
+
for (const canonicalPath of ambiguousCanonical) {
|
|
282
|
+
const indexedCanonical = indexedFile(root, canonicalPath, runGit);
|
|
283
|
+
const indexedMirror = indexedFile(root, `.agents/${canonicalPath}`, runGit);
|
|
284
|
+
if (indexedCanonical === null ? indexedMirror !== null : indexedMirror === null || !indexedCanonical.equals(indexedMirror)) {
|
|
285
|
+
throw new Error(`canonical index differs from generated mirror index: ${canonicalPath}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (ambiguousCanonical.size > 0) {
|
|
289
|
+
throw new Error(`unstaged canonical skill changes must be reconciled before mirror sync: ${[...ambiguousCanonical].join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const changed = changedSkillFiles(root, runGit, ignoredCanonical);
|
|
293
|
+
const authorizations = [];
|
|
294
|
+
for (const file of changed) {
|
|
295
|
+
const authorization = await issueAuthorization(root, {
|
|
296
|
+
actor,
|
|
297
|
+
path: file.path,
|
|
298
|
+
sourceHead,
|
|
299
|
+
writeIntent: file.writeIntent,
|
|
300
|
+
...(file.writeIntent === 'delete' ? { priorContent: file.canonical } : {}),
|
|
53
301
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
302
|
+
if (!authorization.success) throw new Error(`authorization failed for ${file.path}: ${authorization.error}`);
|
|
303
|
+
authorizations.push({ ...file, capabilityId: authorization.capabilityId });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const refreshedIgnoredCanonical = strictIgnoredCanonicalPaths(root, runGit);
|
|
307
|
+
rejectIgnoredSkillDescriptors(root, refreshedIgnoredCanonical);
|
|
308
|
+
const refreshedAmbiguousCanonical = ambiguousCanonicalPaths(root, runGit);
|
|
309
|
+
if (refreshedAmbiguousCanonical.size > 0) {
|
|
310
|
+
throw new Error(`canonical skill state changed during authorization: ${[...refreshedAmbiguousCanonical].join(', ')}`);
|
|
311
|
+
}
|
|
312
|
+
const refreshedChanged = changedSkillFiles(root, runGit, refreshedIgnoredCanonical);
|
|
313
|
+
if (!sameSkillChanges(changed, refreshedChanged)) {
|
|
314
|
+
throw new Error('canonical skill state changed during authorization');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const excludeRelativePaths = new Set([...refreshedIgnoredCanonical]
|
|
318
|
+
.map(repoPath => repoPath.slice('skills/'.length)));
|
|
319
|
+
const { written } = populateCodexRepoSkills({
|
|
320
|
+
sourceRoot: root,
|
|
321
|
+
projectRoot: root,
|
|
322
|
+
clean: true,
|
|
323
|
+
excludeRelativePaths,
|
|
324
|
+
});
|
|
325
|
+
for (const file of authorizations) {
|
|
326
|
+
const mirrorExists = fs.existsSync(path.join(root, file.path));
|
|
327
|
+
if (file.writeIntent === 'delete' ? mirrorExists : !file.canonical.equals(fs.readFileSync(path.join(root, file.path)))) {
|
|
328
|
+
throw new Error(`canonical mirror intent failed for ${file.path}`);
|
|
329
|
+
}
|
|
330
|
+
const completion = await completeAuthorization(root, {
|
|
331
|
+
actor,
|
|
332
|
+
path: file.path,
|
|
333
|
+
sourceHead,
|
|
334
|
+
capabilityId: file.capabilityId,
|
|
335
|
+
writeIntent: file.writeIntent,
|
|
336
|
+
...(file.writeIntent === 'delete' ? { priorContent: file.canonical } : {}),
|
|
337
|
+
});
|
|
338
|
+
if (!completion.success) throw new Error(`completion failed for ${file.path}: ${completion.error}`);
|
|
57
339
|
}
|
|
58
340
|
|
|
341
|
+
const mirror = resolveCodexRepoSkillsDir(root);
|
|
342
|
+
runGit('git', ['add', '--all', '--', mirror], {
|
|
343
|
+
cwd: root,
|
|
344
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
345
|
+
});
|
|
59
346
|
console.log(`sync-agent-skills: .agents/skills in sync with skills/ (${written.length} skills)`);
|
|
347
|
+
return { written, changed: changed.map(file => file.path), deferred: [], sourceHead };
|
|
60
348
|
}
|
|
61
349
|
|
|
62
|
-
main()
|
|
350
|
+
async function main() {
|
|
351
|
+
try {
|
|
352
|
+
await syncAgentSkills();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`sync-agent-skills: failed — ${error.message}`);
|
|
355
|
+
process.exitCode = 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (require.main === module) main();
|
|
360
|
+
|
|
361
|
+
module.exports = { actorFromEnv, changedSkillFiles, parseGitPaths, syncAgentSkills };
|