@ikon85/agent-workflow-kit 0.10.0
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/skills/ask-matt/SKILL.md +139 -0
- package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/audit-skills/SKILL.md +145 -0
- package/.agents/skills/board-to-waves/SKILL.md +135 -0
- package/.agents/skills/code-review/SKILL.md +64 -0
- package/.agents/skills/codebase-design/DEEPENING.md +37 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.agents/skills/codebase-design/SKILL.md +114 -0
- package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
- package/.agents/skills/decision-gate/SKILL.md +67 -0
- package/.agents/skills/diagnose/SKILL.md +136 -0
- package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +76 -0
- package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
- package/.agents/skills/grill-me/SKILL.md +54 -0
- package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/grill-with-docs/SKILL.md +137 -0
- package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/implement/SKILL.md +15 -0
- package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/kit-release/SKILL.md +56 -0
- package/.agents/skills/local-ci/SKILL.md +94 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.agents/skills/prototype/LOGIC.md +79 -0
- package/.agents/skills/prototype/SKILL.md +30 -0
- package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.agents/skills/research/SKILL.md +12 -0
- package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/retro/SKILL.md +209 -0
- package/.agents/skills/scale-check/SKILL.md +141 -0
- package/.agents/skills/security-audit/SKILL.md +67 -0
- package/.agents/skills/setup-workflow/SKILL.md +187 -0
- package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/setup-workflow/board-sync.md +111 -0
- package/.agents/skills/setup-workflow/code-review.md +26 -0
- package/.agents/skills/setup-workflow/domain.md +51 -0
- package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.agents/skills/setup-workflow/triage-labels.md +18 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
- package/.agents/skills/spec-self-critique/SKILL.md +152 -0
- package/.agents/skills/spec-self-critique/scenarios.md +82 -0
- package/.agents/skills/tdd/SKILL.md +120 -0
- package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/to-issues/SKILL.md +325 -0
- package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.agents/skills/to-prd/SKILL.md +180 -0
- package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +260 -0
- package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
- package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.agents/skills/triage/SKILL.md +122 -0
- package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/verify-spike/SKILL.md +39 -0
- package/.agents/skills/wayfinder/SKILL.md +127 -0
- package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/wrapup/SKILL.md +71 -0
- package/.agents/skills/wrapup/SOURCES.txt +6 -0
- package/.agents/skills/zoom-out/SKILL.md +7 -0
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/hooks/_hook_utils.py +184 -0
- package/.claude/hooks/drift-guard.py +176 -0
- package/.claude/hooks/skill-drift-hint.py +122 -0
- package/.claude/hooks/sync-board-status.py +191 -0
- package/.claude/skills/ask-matt/SKILL.md +139 -0
- package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/audit-skills/SKILL.md +145 -0
- package/.claude/skills/board-to-waves/SKILL.md +148 -0
- package/.claude/skills/code-review/SKILL.md +64 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/codex-build/SKILL.md +126 -0
- package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
- package/.claude/skills/codex-review/SKILL.md +145 -0
- package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
- package/.claude/skills/decision-gate/SKILL.md +67 -0
- package/.claude/skills/diagnose/SKILL.md +136 -0
- package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +76 -0
- package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
- package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
- package/.claude/skills/grill-me/SKILL.md +54 -0
- package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-me-codex/SKILL.md +158 -0
- package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
- package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs/SKILL.md +137 -0
- package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
- package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
- package/.claude/skills/implement/SKILL.md +15 -0
- package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/kit-release/SKILL.md +56 -0
- package/.claude/skills/local-ci/SKILL.md +94 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.claude/skills/prototype/LOGIC.md +79 -0
- package/.claude/skills/prototype/SKILL.md +30 -0
- package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/prototype/UI.md +112 -0
- package/.claude/skills/research/SKILL.md +12 -0
- package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/retro/SKILL.md +209 -0
- package/.claude/skills/scale-check/SKILL.md +141 -0
- package/.claude/skills/security-audit/SKILL.md +67 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
- package/.claude/skills/setup-workflow/SKILL.md +187 -0
- package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/setup-workflow/board-sync.md +111 -0
- package/.claude/skills/setup-workflow/code-review.md +26 -0
- package/.claude/skills/setup-workflow/domain.md +51 -0
- package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.claude/skills/setup-workflow/triage-labels.md +18 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
- package/.claude/skills/skill-manifest.json +389 -0
- package/.claude/skills/spec-self-critique/SKILL.md +152 -0
- package/.claude/skills/spec-self-critique/scenarios.md +82 -0
- package/.claude/skills/tdd/SKILL.md +120 -0
- package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/tdd/deep-modules.md +33 -0
- package/.claude/skills/tdd/interface-design.md +31 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/refactoring.md +10 -0
- package/.claude/skills/tdd/tests.md +61 -0
- package/.claude/skills/to-issues/SKILL.md +325 -0
- package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.claude/skills/to-prd/SKILL.md +180 -0
- package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +260 -0
- package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.claude/skills/triage/SKILL.md +122 -0
- package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/verify-spike/SKILL.md +39 -0
- package/.claude/skills/wayfinder/SKILL.md +127 -0
- package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/wrapup/SKILL.md +81 -0
- package/.claude/skills/wrapup/SOURCES.txt +6 -0
- package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
- package/.claude/skills/write-a-skill/SKILL.md +88 -0
- package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/zoom-out/SKILL.md +7 -0
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/LICENSE +28 -0
- package/PROVENANCE.md +39 -0
- package/README.md +619 -0
- package/agent-workflow-kit.package.json +1939 -0
- package/docs/agents/board-sync.md +71 -0
- package/docs/agents/code-review.md +14 -0
- package/docs/agents/domain.md +7 -0
- package/docs/agents/issue-tracker.md +12 -0
- package/docs/agents/security-audit-runbook-template.md +205 -0
- package/docs/agents/skills/orchestrate-wave.md +13 -0
- package/docs/agents/skills/spec-self-critique.md +18 -0
- package/docs/agents/triage-labels.md +10 -0
- package/docs/agents/wave-anchor-template.md +88 -0
- package/docs/conventions/spec-completeness.md +23 -0
- package/docs/index.html +56 -0
- package/docs/methodology.html +488 -0
- package/docs/methodology.svg +93 -0
- package/docs/workflow.png +0 -0
- package/package.json +40 -0
- package/scripts/anchor_table.py +343 -0
- package/scripts/board-sync.py +1232 -0
- package/scripts/board_config.py +147 -0
- package/scripts/board_fields.py +192 -0
- package/scripts/build-kit.mjs +116 -0
- package/scripts/build-kit.test.mjs +59 -0
- package/scripts/check-kit-staleness.mjs +45 -0
- package/scripts/check-kit-staleness.test.mjs +19 -0
- package/scripts/execute-ready-check.py +516 -0
- package/scripts/issue_deps.py +79 -0
- package/scripts/kit-release.mjs +100 -0
- package/scripts/kit-release.test.mjs +67 -0
- package/scripts/lib/audit-refs.mjs +146 -0
- package/scripts/lib/scrub.mjs +113 -0
- package/scripts/lib/scrub.test.mjs +187 -0
- package/scripts/loc_offender_core.py +196 -0
- package/scripts/loc_offender_gate.py +182 -0
- package/scripts/node_kind.py +68 -0
- package/scripts/portability_profile_scan.py +53 -0
- package/scripts/pr-body-check.py +263 -0
- package/scripts/program_graph.py +125 -0
- package/scripts/program_graph_parse.py +275 -0
- package/scripts/program_graph_validate.py +256 -0
- package/scripts/program_sync.py +160 -0
- package/scripts/release-delta-guard.mjs +96 -0
- package/scripts/release-delta-guard.test.mjs +53 -0
- package/scripts/test_board_sync_create_idempotency.py +104 -0
- package/scripts/test_dist_kit_smoke.py +199 -0
- package/scripts/test_kit_docs_language_census.py +178 -0
- package/scripts/test_skill_code_review_seed.py +89 -0
- package/scripts/test_skill_frontmatter_lint.py +226 -0
- package/scripts/test_skill_gh_lint.py +124 -0
- package/scripts/test_skill_language_census.py +254 -0
- package/scripts/test_skill_portability_lint.py +773 -0
- package/scripts/test_skill_publish_audit.py +300 -0
- package/scripts/test_skill_selfcontainment_lint.py +255 -0
- package/scripts/test_skill_setup_workflow_seeds.py +200 -0
- package/scripts/test_skill_stale_name_lint.py +126 -0
- package/scripts/test_skill_surface_refs.py +300 -0
- package/scripts/test_skill_trailing_artifact_lint.py +71 -0
- package/scripts/wrapup-land.py +573 -0
- package/src/cli.mjs +70 -0
- package/src/commands/diff.mjs +6 -0
- package/src/commands/init.mjs +59 -0
- package/src/commands/uninstall.mjs +43 -0
- package/src/commands/update.mjs +95 -0
- package/src/lib/atomicWrite.mjs +49 -0
- package/src/lib/bundle.mjs +133 -0
- package/src/lib/hash.mjs +13 -0
- package/src/lib/manifest.mjs +58 -0
- package/src/lib/sentinel.mjs +20 -0
- package/src/lib/settings.mjs +23 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { init } from './commands/init.mjs';
|
|
6
|
+
import { update } from './commands/update.mjs';
|
|
7
|
+
import { diff } from './commands/diff.mjs';
|
|
8
|
+
import { uninstall } from './commands/uninstall.mjs';
|
|
9
|
+
|
|
10
|
+
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const consumerRoot = process.cwd();
|
|
12
|
+
|
|
13
|
+
function stamp() {
|
|
14
|
+
// backup suffix: YYYYMMDDTHHMMSS (no separators that collide with shells)
|
|
15
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const cmd = args[0];
|
|
20
|
+
const force = args.includes('--force');
|
|
21
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
22
|
+
|
|
23
|
+
p.intro('agent-workflow-kit');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (cmd === 'init') {
|
|
27
|
+
const r = await init({ kitRoot: KIT_ROOT, consumerRoot, force });
|
|
28
|
+
p.note(
|
|
29
|
+
`copied ${r.copied.length} · seeded ${r.seeded.length} stub(s)` +
|
|
30
|
+
(r.skipped.length ? `\nskipped (pre-existing, use --force): ${r.skipped.join(', ')}` : ''),
|
|
31
|
+
'init'
|
|
32
|
+
);
|
|
33
|
+
p.outro('Next: run /setup-workflow to fill the project layer + board profile. ' +
|
|
34
|
+
'To enable the drift-guard hook, add .claude/hooks/drift-guard.py to your settings.json hooks.');
|
|
35
|
+
} else if (cmd === 'diff') {
|
|
36
|
+
const r = await diff({ kitRoot: KIT_ROOT, consumerRoot });
|
|
37
|
+
printPlan(r);
|
|
38
|
+
p.outro('Dry run — nothing written. Run `update` to apply.');
|
|
39
|
+
} else if (cmd === 'update') {
|
|
40
|
+
const decide = async (_action, path) => {
|
|
41
|
+
if (yes) return true;
|
|
42
|
+
const ok = await p.confirm({ message: `Upstream removed ${path} — delete it locally?` });
|
|
43
|
+
return ok === true;
|
|
44
|
+
};
|
|
45
|
+
const r = await update({ kitRoot: KIT_ROOT, consumerRoot, now: stamp(), decide });
|
|
46
|
+
printPlan(r);
|
|
47
|
+
for (const c of r.conflicts) p.note(c.diff || '(binary/!text)', `conflict (backed up): ${c.path}`);
|
|
48
|
+
p.outro(`updated ${r.updated.length} · added ${r.added.length} · ` +
|
|
49
|
+
`conflicts ${r.conflicts.length} · deleted ${r.deleted.length}`);
|
|
50
|
+
} else if (cmd === 'uninstall') {
|
|
51
|
+
const ok = yes || (await p.confirm({ message: 'Remove kit-installed files?' })) === true;
|
|
52
|
+
if (!ok) { p.cancel('Aborted.'); process.exit(0); }
|
|
53
|
+
const r = await uninstall({ consumerRoot });
|
|
54
|
+
p.outro(`removed ${r.removed.length} · retained (edited/referenced) ${r.retained.length}`);
|
|
55
|
+
} else {
|
|
56
|
+
p.note('Usage: agent-workflow-kit <init|update|diff|uninstall> [--force] [--yes]');
|
|
57
|
+
p.outro('');
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
p.cancel(`Error: ${err.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printPlan(r) {
|
|
65
|
+
const lines = [];
|
|
66
|
+
for (const k of ['added', 'updated', 'userModified', 'unchanged', 'deleted', 'keptDeleted'])
|
|
67
|
+
if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
|
|
68
|
+
if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
|
|
69
|
+
p.note(lines.join('\n') || 'no changes', 'plan');
|
|
70
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile, access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha256File } from '../lib/hash.mjs';
|
|
4
|
+
import { writeAtomic } from '../lib/atomicWrite.mjs';
|
|
5
|
+
import { stubSentinel } from '../lib/sentinel.mjs';
|
|
6
|
+
import { STUB_TARGETS } from '../lib/bundle.mjs';
|
|
7
|
+
import {
|
|
8
|
+
readManifest, writeManifest, emptyConsumerManifest,
|
|
9
|
+
PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
|
|
10
|
+
} from '../lib/manifest.mjs';
|
|
11
|
+
|
|
12
|
+
const exists = (p) => access(p).then(() => true, () => false);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Install the kit into a consumer repo.
|
|
16
|
+
* - copies every package-manifest file (never-clobbering a pre-existing UNTRACKED
|
|
17
|
+
* file unless `force`),
|
|
18
|
+
* - writes the consumer manifest,
|
|
19
|
+
* - seeds the doc-layer stubs with the setup-workflow sentinel (skips any that
|
|
20
|
+
* already exist — idempotent),
|
|
21
|
+
* - never touches board-sync.md / CLAUDE.md / AGENTS.md.
|
|
22
|
+
*/
|
|
23
|
+
export async function init({ kitRoot, consumerRoot, force = false }) {
|
|
24
|
+
const pkg = await readManifest(join(kitRoot, PACKAGE_MANIFEST_NAME));
|
|
25
|
+
if (!pkg) throw new Error('kit package manifest not found');
|
|
26
|
+
const prior = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
27
|
+
const tracked = new Set((prior?.installed ?? []).map((e) => e.path));
|
|
28
|
+
|
|
29
|
+
const result = { copied: [], skipped: [], seeded: [] };
|
|
30
|
+
const installed = [];
|
|
31
|
+
|
|
32
|
+
for (const f of pkg.files) {
|
|
33
|
+
const dest = join(consumerRoot, f.path);
|
|
34
|
+
if (await exists(dest) && !tracked.has(f.path) && !force) {
|
|
35
|
+
result.skipped.push(f.path); // pre-existing untracked → never-clobber
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
|
|
39
|
+
installed.push({
|
|
40
|
+
path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface,
|
|
41
|
+
installedSha256: await sha256File(dest), origin: 'kit',
|
|
42
|
+
});
|
|
43
|
+
result.copied.push(f.path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await writeManifest(
|
|
47
|
+
join(consumerRoot, CONSUMER_MANIFEST_NAME),
|
|
48
|
+
{ ...emptyConsumerManifest(pkg.kitVersion), installed }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const stub of STUB_TARGETS) {
|
|
52
|
+
const dest = join(consumerRoot, stub);
|
|
53
|
+
if (await exists(dest)) continue; // already present (stub or filled) → leave it
|
|
54
|
+
await writeAtomic(dest, stubSentinel() + '\n');
|
|
55
|
+
result.seeded.push(stub);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { access, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha256File } from '../lib/hash.mjs';
|
|
4
|
+
import { hookReferenced } from '../lib/settings.mjs';
|
|
5
|
+
import { writeManifest, readManifest, emptyConsumerManifest, CONSUMER_MANIFEST_NAME } from '../lib/manifest.mjs';
|
|
6
|
+
|
|
7
|
+
const exists = (p) => access(p).then(() => true, () => false);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Remove kit-installed files. User-edited files and hook files still referenced
|
|
11
|
+
* by settings are retained (Codex R1#11 / R3#7). If anything is retained, the
|
|
12
|
+
* manifest is kept with those entries marked `orphanedByUninstall`; on a fully
|
|
13
|
+
* clean uninstall the manifest is removed.
|
|
14
|
+
*/
|
|
15
|
+
export async function uninstall({ consumerRoot }) {
|
|
16
|
+
const consumer = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
17
|
+
if (!consumer) throw new Error('not initialised — nothing to uninstall');
|
|
18
|
+
|
|
19
|
+
const res = { removed: [], retained: [] };
|
|
20
|
+
const retainedEntries = [];
|
|
21
|
+
|
|
22
|
+
for (const e of consumer.installed) {
|
|
23
|
+
const dest = join(consumerRoot, e.path);
|
|
24
|
+
if (!(await exists(dest))) continue; // already gone
|
|
25
|
+
const userEdited = (await sha256File(dest)) !== e.installedSha256;
|
|
26
|
+
const referenced = e.kind === 'hook' && (await hookReferenced(consumerRoot, e.path));
|
|
27
|
+
if (userEdited || referenced) {
|
|
28
|
+
retainedEntries.push({ ...e, orphanedByUninstall: true });
|
|
29
|
+
res.retained.push(e.path);
|
|
30
|
+
} else {
|
|
31
|
+
await rm(dest);
|
|
32
|
+
res.removed.push(e.path);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const manifestPath = join(consumerRoot, CONSUMER_MANIFEST_NAME);
|
|
37
|
+
if (retainedEntries.length) {
|
|
38
|
+
await writeManifest(manifestPath, { ...emptyConsumerManifest(consumer.kitVersion), installed: retainedEntries });
|
|
39
|
+
} else {
|
|
40
|
+
await rm(manifestPath, { force: true });
|
|
41
|
+
}
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile, access, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha256File } from '../lib/hash.mjs';
|
|
4
|
+
import { writeAtomic, backupFile, lineDiff } from '../lib/atomicWrite.mjs';
|
|
5
|
+
import { hookReferenced } from '../lib/settings.mjs';
|
|
6
|
+
import {
|
|
7
|
+
readManifest, writeManifest, emptyConsumerManifest,
|
|
8
|
+
PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME, indexByPath,
|
|
9
|
+
} from '../lib/manifest.mjs';
|
|
10
|
+
|
|
11
|
+
const exists = (p) => access(p).then(() => true, () => false);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reconcile an installed consumer against the current kit (three-way: installed
|
|
15
|
+
* hash vs current file = user edit?, vs package desired = upstream change?).
|
|
16
|
+
* - unmodified + upstream changed → atomic overwrite (`updated`)
|
|
17
|
+
* - user-edited + upstream changed → timestamped backup + diff, NOT clobbered (`conflicts`)
|
|
18
|
+
* - user-edited + upstream same → left as-is (`userModified`)
|
|
19
|
+
* - new upstream file → installed (`added`)
|
|
20
|
+
* - upstream-removed + unmodified → offered for deletion via `decide` (`deleted`/`keptDeleted`)
|
|
21
|
+
* - a hook file referenced by the consumer's settings is never auto-deleted (R3#7)
|
|
22
|
+
* `dryRun` classifies without writing (powers `diff`). `now` = backup stamp.
|
|
23
|
+
*/
|
|
24
|
+
export async function update({ kitRoot, consumerRoot, now, decide = () => false, dryRun = false }) {
|
|
25
|
+
const pkg = await readManifest(join(kitRoot, PACKAGE_MANIFEST_NAME));
|
|
26
|
+
if (!pkg) throw new Error('kit package manifest not found');
|
|
27
|
+
const consumer = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
28
|
+
if (!consumer) throw new Error('not initialised — run `init` first');
|
|
29
|
+
|
|
30
|
+
const installedIdx = indexByPath(consumer, 'installed');
|
|
31
|
+
const pkgIdx = indexByPath(pkg, 'files');
|
|
32
|
+
const res = { unchanged: [], updated: [], conflicts: [], userModified: [], added: [], deleted: [], keptDeleted: [] };
|
|
33
|
+
const nextInstalled = [];
|
|
34
|
+
|
|
35
|
+
for (const f of pkg.files) {
|
|
36
|
+
const dest = join(consumerRoot, f.path);
|
|
37
|
+
const prior = installedIdx.get(f.path);
|
|
38
|
+
const cur = (await exists(dest)) ? await sha256File(dest) : null;
|
|
39
|
+
|
|
40
|
+
if (!prior || cur === null) {
|
|
41
|
+
if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
|
|
42
|
+
nextInstalled.push(entry(f, f.sha256));
|
|
43
|
+
res.added.push(f.path);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const userEdited = cur !== prior.installedSha256;
|
|
47
|
+
const upstreamChanged = f.sha256 !== prior.installedSha256;
|
|
48
|
+
|
|
49
|
+
if (!userEdited && upstreamChanged) {
|
|
50
|
+
if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
|
|
51
|
+
nextInstalled.push(entry(f, f.sha256));
|
|
52
|
+
res.updated.push(f.path);
|
|
53
|
+
} else if (userEdited && upstreamChanged) {
|
|
54
|
+
const incoming = await readFile(join(kitRoot, f.path), 'utf8');
|
|
55
|
+
const diff = lineDiff(await readFile(dest, 'utf8'), incoming);
|
|
56
|
+
if (!dryRun) await backupFile(dest, now);
|
|
57
|
+
nextInstalled.push(prior); // keep installed-hash baseline; file stays user's
|
|
58
|
+
res.conflicts.push({ path: f.path, diff });
|
|
59
|
+
} else if (userEdited) {
|
|
60
|
+
nextInstalled.push(prior);
|
|
61
|
+
res.userModified.push(f.path);
|
|
62
|
+
} else {
|
|
63
|
+
nextInstalled.push(prior);
|
|
64
|
+
res.unchanged.push(f.path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// upstream-removed files (in consumer manifest, gone from package)
|
|
69
|
+
for (const prior of consumer.installed) {
|
|
70
|
+
if (pkgIdx.has(prior.path)) continue;
|
|
71
|
+
const dest = join(consumerRoot, prior.path);
|
|
72
|
+
const cur = (await exists(dest)) ? await sha256File(dest) : null;
|
|
73
|
+
const userEdited = cur !== null && cur !== prior.installedSha256;
|
|
74
|
+
const referenced = prior.kind === 'hook' && (await hookReferenced(consumerRoot, prior.path));
|
|
75
|
+
if (userEdited || referenced || !(await decide('delete', prior.path))) {
|
|
76
|
+
nextInstalled.push(prior);
|
|
77
|
+
res.keptDeleted.push(prior.path);
|
|
78
|
+
} else {
|
|
79
|
+
if (!dryRun && cur !== null) await rm(dest);
|
|
80
|
+
res.deleted.push(prior.path);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!dryRun) {
|
|
85
|
+
await writeManifest(
|
|
86
|
+
join(consumerRoot, CONSUMER_MANIFEST_NAME),
|
|
87
|
+
{ ...emptyConsumerManifest(pkg.kitVersion), installed: nextInstalled }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function entry(f, sha) {
|
|
94
|
+
return { path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface, installedSha256: sha, origin: 'kit' };
|
|
95
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { writeFile, rename, mkdir, copyFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write `content` to `path` atomically: write a sibling temp file, then rename
|
|
6
|
+
* (atomic on the same filesystem). Creates parent dirs. Preserves `mode` if given.
|
|
7
|
+
*/
|
|
8
|
+
export async function writeAtomic(path, content, mode) {
|
|
9
|
+
await mkdir(dirname(path), { recursive: true });
|
|
10
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
11
|
+
const opts = mode === undefined ? 'utf8' : { encoding: 'utf8', mode };
|
|
12
|
+
await writeFile(tmp, content, opts);
|
|
13
|
+
await rename(tmp, path);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copy an existing file to `path.<stamp>.bak` (caller supplies the stamp so the
|
|
18
|
+
* name never collides and tests stay deterministic). Returns the backup path.
|
|
19
|
+
*/
|
|
20
|
+
export async function backupFile(path, stamp) {
|
|
21
|
+
const bak = `${path}.${stamp}.bak`;
|
|
22
|
+
await copyFile(path, bak);
|
|
23
|
+
return bak;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimal LCS-based line diff → unified-ish text. Context lines prefixed ' ',
|
|
28
|
+
* removals '-', additions '+'. Empty string when the inputs are identical.
|
|
29
|
+
*/
|
|
30
|
+
export function lineDiff(oldText, newText) {
|
|
31
|
+
const a = oldText.split('\n');
|
|
32
|
+
const b = newText.split('\n');
|
|
33
|
+
// LCS table
|
|
34
|
+
const m = a.length, n = b.length;
|
|
35
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
36
|
+
for (let i = m - 1; i >= 0; i--)
|
|
37
|
+
for (let j = n - 1; j >= 0; j--)
|
|
38
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
39
|
+
const out = [];
|
|
40
|
+
let i = 0, j = 0;
|
|
41
|
+
while (i < m && j < n) {
|
|
42
|
+
if (a[i] === b[j]) { out.push(' ' + a[i]); i++; j++; }
|
|
43
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) { out.push('-' + a[i]); i++; }
|
|
44
|
+
else { out.push('+' + b[j]); j++; }
|
|
45
|
+
}
|
|
46
|
+
while (i < m) out.push('-' + a[i++]);
|
|
47
|
+
while (j < n) out.push('+' + b[j++]);
|
|
48
|
+
return out.some((l) => l[0] !== ' ') ? out.join('\n') : '';
|
|
49
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The planning skills are not usable without their helper ecosystem (Codex R2#1).
|
|
5
|
+
// These ship alongside the skills. Paths are relative to the bundle/consumer root.
|
|
6
|
+
export const HELPER_FILES = [
|
|
7
|
+
// Shared profile loader imported by the three planning scripts — they read
|
|
8
|
+
// every board-specific value from docs/agents/board-sync.md through it, so it
|
|
9
|
+
// MUST ship or they are broken-on-arrival. Library (imported, not run) → 0o644.
|
|
10
|
+
{ path: 'scripts/board_config.py', kind: 'script', mode: 0o644 },
|
|
11
|
+
// Pure Slices-table logic imported by board-sync.py for `anchor-sync` —
|
|
12
|
+
// library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
|
|
13
|
+
{ path: 'scripts/anchor_table.py', kind: 'script', mode: 0o644 },
|
|
14
|
+
// Programm-Graph module for `board-sync.py validate-graph` — split by
|
|
15
|
+
// concern (parse/validate) across three files, all libraries (imported, not
|
|
16
|
+
// run) → 0o644. MUST ship together or board-sync.py ImportErrors.
|
|
17
|
+
{ path: 'scripts/program_graph.py', kind: 'script', mode: 0o644 },
|
|
18
|
+
{ path: 'scripts/program_graph_parse.py', kind: 'script', mode: 0o644 },
|
|
19
|
+
{ path: 'scripts/program_graph_validate.py', kind: 'script', mode: 0o644 },
|
|
20
|
+
// Pure node-kind classifier imported by execute-ready-check.py —
|
|
21
|
+
// library (imported, not run) → 0o644. MUST ship or execute-ready-check.py ImportErrors.
|
|
22
|
+
{ path: 'scripts/node_kind.py', kind: 'script', mode: 0o644 },
|
|
23
|
+
// stamp-batch / field-value / promote-guard logic for board-sync.py —
|
|
24
|
+
// library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
|
|
25
|
+
{ path: 'scripts/board_fields.py', kind: 'script', mode: 0o644 },
|
|
26
|
+
// Wellenplan Status-resync for board-sync.py's `program-sync` —
|
|
27
|
+
// library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
|
|
28
|
+
{ path: 'scripts/program_sync.py', kind: 'script', mode: 0o644 },
|
|
29
|
+
// Blocked-by body-mirror logic for the native issue-dependency commands
|
|
30
|
+
// — imported by BOTH board-sync.py (dep-add/dep-remove) and
|
|
31
|
+
// execute-ready-check.py (drift check). Library (imported, not run) → 0o644.
|
|
32
|
+
// MUST ship or both ImportError on arrival.
|
|
33
|
+
{ path: 'scripts/issue_deps.py', kind: 'script', mode: 0o644 },
|
|
34
|
+
{ path: 'scripts/board-sync.py', kind: 'script', mode: 0o755 },
|
|
35
|
+
{ path: 'scripts/execute-ready-check.py', kind: 'script', mode: 0o755 },
|
|
36
|
+
{ path: 'scripts/pr-body-check.py', kind: 'script', mode: 0o755 },
|
|
37
|
+
// Mechanical executor for /wrapup (preflight/commit/land) — replaced the
|
|
38
|
+
// Sonnet phase-2 subagent. Imports board_config + anchor_table
|
|
39
|
+
// (both shipped above). Invokable CLI → 0o755.
|
|
40
|
+
{ path: 'scripts/wrapup-land.py', kind: 'script', mode: 0o755 },
|
|
41
|
+
// Deterministic release preparation and its manifest-derived local/CI guard.
|
|
42
|
+
// Both are invoked through package scripts by /kit-release.
|
|
43
|
+
{ path: 'scripts/kit-release.mjs', kind: 'script', mode: 0o644 },
|
|
44
|
+
{ path: 'scripts/release-delta-guard.mjs', kind: 'script', mode: 0o644 },
|
|
45
|
+
// Shared hook utility imported by the shipped hooks (drift-guard,
|
|
46
|
+
// sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
|
|
47
|
+
// hooks ImportError on arrival.
|
|
48
|
+
{ path: '.claude/hooks/_hook_utils.py', kind: 'hook', mode: 0o644 },
|
|
49
|
+
{ path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
|
|
50
|
+
// SessionStart skill-freshness drift-hint (audit-skills names it). For each
|
|
51
|
+
// <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports
|
|
52
|
+
// _hook_utils (shipped above); stdlib-only otherwise. Executable hook → 0o755.
|
|
53
|
+
{ path: '.claude/hooks/skill-drift-hint.py', kind: 'hook', mode: 0o755 },
|
|
54
|
+
// Board-status pickup hook — profile-driven (reads project/field/status ids
|
|
55
|
+
// from the consumer-seeded board profile), so it ships portably. /tdd names it.
|
|
56
|
+
{ path: '.claude/hooks/sync-board-status.py', kind: 'hook', mode: 0o755 },
|
|
57
|
+
{ path: 'docs/agents/wave-anchor-template.md', kind: 'template', mode: 0o644 },
|
|
58
|
+
// Part-0–5 security-audit runbook skeleton (the security-audit skill names it).
|
|
59
|
+
// The stack-coupled checklist is deliberately NOT shipped — this template is the
|
|
60
|
+
// generic structure a consumer copies + fills stack-specifically. Prose → 0o644.
|
|
61
|
+
{ path: 'docs/agents/security-audit-runbook-template.md', kind: 'template', mode: 0o644 },
|
|
62
|
+
// Opt-in LoC-offender drive gate (setup-workflow §7b names it). Both are
|
|
63
|
+
// stdlib-only and profile-driven (threshold + offenders read from the
|
|
64
|
+
// consumer-seeded max-lines-allowlist.json), so they ship portably. The gate
|
|
65
|
+
// imports the core, so the core MUST ship too. core = library → 0o644;
|
|
66
|
+
// gate = invokable CLI (pre-push / manual) → 0o755.
|
|
67
|
+
{ path: 'scripts/loc_offender_core.py', kind: 'script', mode: 0o644 },
|
|
68
|
+
{ path: 'scripts/loc_offender_gate.py', kind: 'script', mode: 0o755 },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Project-layer docs `init` seeds as empty stubs (sentinel first line). NOT
|
|
72
|
+
// board-sync.md — that is discovery-dependent, /setup-workflow classifies+fills it
|
|
73
|
+
// (Codex R1#8).
|
|
74
|
+
export const STUB_TARGETS = [
|
|
75
|
+
'docs/agents/issue-tracker.md',
|
|
76
|
+
'docs/agents/triage-labels.md',
|
|
77
|
+
'docs/agents/domain.md',
|
|
78
|
+
'docs/agents/skills/spec-self-critique.md',
|
|
79
|
+
'docs/agents/skills/orchestrate-wave.md',
|
|
80
|
+
'docs/agents/skills/local-ci.md',
|
|
81
|
+
'docs/agents/skills/git-worktree-recover.md',
|
|
82
|
+
'docs/agents/skills/audit-skills.md',
|
|
83
|
+
'docs/agents/skills/security-audit.md',
|
|
84
|
+
'docs/conventions/spec-completeness.md',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const SURFACE_DIR = { claude: '.claude/skills', codex: '.agents/skills' };
|
|
88
|
+
|
|
89
|
+
/** Skills with publish:true, each with the surfaces it installs into. */
|
|
90
|
+
export function publishableSkills(manifest) {
|
|
91
|
+
return Object.entries(manifest.skills)
|
|
92
|
+
.filter(([, e]) => e.publish)
|
|
93
|
+
.map(([name, e]) => ({ name, surfaces: e.surfaces }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function walk(dir) {
|
|
97
|
+
const out = [];
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err.code === 'ENOENT') return out;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
const p = join(dir, e.name);
|
|
107
|
+
if (e.isDirectory()) out.push(...(await walk(p)));
|
|
108
|
+
else if (e.isFile()) out.push(p);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Every file to copy from the kit into a consumer, plus the doc stubs to seed.
|
|
115
|
+
* Returns { files: [{ src, dest, kind, ownerSkill?, surface?, mode }], stubs: [paths] }.
|
|
116
|
+
* `src`/`dest` are repo-root-relative (same layout in kit and consumer).
|
|
117
|
+
*/
|
|
118
|
+
export async function collectBundle(repoRoot, manifest) {
|
|
119
|
+
const files = [];
|
|
120
|
+
for (const { name, surfaces } of publishableSkills(manifest)) {
|
|
121
|
+
for (const surface of surfaces) {
|
|
122
|
+
const base = join(SURFACE_DIR[surface], name);
|
|
123
|
+
for (const abs of await walk(join(repoRoot, base))) {
|
|
124
|
+
const rel = relative(repoRoot, abs);
|
|
125
|
+
files.push({ src: rel, dest: rel, kind: 'skill', ownerSkill: name, surface, mode: 0o644 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const h of HELPER_FILES) {
|
|
130
|
+
files.push({ src: h.path, dest: h.path, kind: h.kind, mode: h.mode });
|
|
131
|
+
}
|
|
132
|
+
return { files, stubs: STUB_TARGETS };
|
|
133
|
+
}
|
package/src/lib/hash.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
/** sha256 hex digest of a file's raw bytes. */
|
|
5
|
+
export async function sha256File(path) {
|
|
6
|
+
const buf = await readFile(path);
|
|
7
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** sha256 hex digest of an in-memory string/buffer. */
|
|
11
|
+
export function sha256(content) {
|
|
12
|
+
return createHash('sha256').update(content).digest('hex');
|
|
13
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { writeAtomic } from './atomicWrite.mjs';
|
|
3
|
+
|
|
4
|
+
// Two manifests (Codex R1#9 / R3#1):
|
|
5
|
+
// - package manifest (shipped with the kit): the desired-state file list.
|
|
6
|
+
// - consumer manifest (agent-workflow-kit.json in the target repo root): installed state.
|
|
7
|
+
// Both model every file kind:
|
|
8
|
+
// { path, kind: 'skill'|'script'|'hook'|'template'|'doc', ownerSkill?, surface?,
|
|
9
|
+
// sha256, mode, origin: 'kit' }
|
|
10
|
+
|
|
11
|
+
export const CONSUMER_MANIFEST_NAME = 'agent-workflow-kit.json';
|
|
12
|
+
export const PACKAGE_MANIFEST_NAME = 'agent-workflow-kit.package.json';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a JSON manifest, or null if the file does not exist. A corrupt file
|
|
16
|
+
* (invalid JSON — e.g. from an aborted write before writeManifest went atomic)
|
|
17
|
+
* throws a clear, recovery-hinting error instead of a raw JSON.parse stack;
|
|
18
|
+
* every command reads this file on startup, so an unreadable manifest bricks
|
|
19
|
+
* init/update/diff/uninstall until the user is told how to recover.
|
|
20
|
+
*/
|
|
21
|
+
export async function readManifest(path) {
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readFile(path, 'utf8');
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err.code === 'ENOENT') return null;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${path} is corrupt (invalid JSON) and can't be read. ` +
|
|
34
|
+
'Restore it from a nearby ".bak" backup, or delete it and re-run `init` to re-track the kit files.',
|
|
35
|
+
{ cause: err }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write a manifest as pretty JSON with a trailing newline, atomically
|
|
42
|
+
* (temp file + rename via writeAtomic) — an abort mid-write must never leave a
|
|
43
|
+
* truncated/corrupt manifest, since every command reads it on startup.
|
|
44
|
+
*/
|
|
45
|
+
export async function writeManifest(path, obj) {
|
|
46
|
+
await writeAtomic(path, JSON.stringify(obj, null, 2) + '\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function emptyConsumerManifest(kitVersion) {
|
|
50
|
+
return { kitVersion, installed: [] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Map a manifest's file list (under `key`) by `path` for quick lookup. */
|
|
54
|
+
export function indexByPath(manifest, key) {
|
|
55
|
+
const idx = new Map();
|
|
56
|
+
for (const entry of (manifest?.[key] ?? [])) idx.set(entry.path, entry);
|
|
57
|
+
return idx;
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// setup-workflow idempotency sentinel (contract). npx init seeds project-layer
|
|
2
|
+
// stub files with this exact first line so /setup-workflow's stub→fill detection works.
|
|
3
|
+
|
|
4
|
+
const RE = /^<!--\s*setup-workflow:\s*state=([a-z-]+)(?:;[^>]*)?\s*-->\s*$/;
|
|
5
|
+
|
|
6
|
+
/** The first line npx init writes onto every seeded project-layer file. */
|
|
7
|
+
export function stubSentinel() {
|
|
8
|
+
return '<!-- setup-workflow: state=stub -->';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* State declared by the FIRST line only (a later mention does not count, per).
|
|
13
|
+
* Returns 'stub' | 'filled' | 'not-applicable' | ... or null if the first line
|
|
14
|
+
* is not a sentinel.
|
|
15
|
+
*/
|
|
16
|
+
export function firstLineState(text) {
|
|
17
|
+
const first = String(text).split('\n', 1)[0];
|
|
18
|
+
const m = RE.exec(first);
|
|
19
|
+
return m ? m[1] : null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SETTINGS = ['.claude/settings.json', '.claude/settings.local.json'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* True if any consumer settings file references the hook (by path or basename).
|
|
8
|
+
* Used so update/uninstall never silently removes a hook that is still wired in
|
|
9
|
+
* (Codex R3#7). Substring match on the raw text — robust to JSON shape.
|
|
10
|
+
*/
|
|
11
|
+
export async function hookReferenced(consumerRoot, hookPath) {
|
|
12
|
+
const needle = basename(hookPath);
|
|
13
|
+
for (const s of SETTINGS) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(join(consumerRoot, s), 'utf8');
|
|
17
|
+
} catch {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (raw.includes(hookPath) || raw.includes(needle)) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|