@delegance/claude-autopilot 5.0.8 → 5.2.1
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/CHANGELOG.md +42 -0
- package/README.md +5 -1
- package/dist/src/cli/autoregress-bridge.js +21 -5
- package/dist/src/cli/index.js +130 -2
- package/dist/src/cli/init-migrate.d.ts +35 -0
- package/dist/src/cli/init-migrate.js +299 -0
- package/dist/src/cli/migrate-doctor.d.ts +19 -0
- package/dist/src/cli/migrate-doctor.js +191 -0
- package/dist/src/core/migrate/alias-resolver.d.ts +18 -0
- package/dist/src/core/migrate/alias-resolver.js +150 -0
- package/dist/src/core/migrate/audit-log.d.ts +30 -0
- package/dist/src/core/migrate/audit-log.js +100 -0
- package/dist/src/core/migrate/contract.d.ts +27 -0
- package/dist/src/core/migrate/contract.js +35 -0
- package/dist/src/core/migrate/detector-rules.d.ts +26 -0
- package/dist/src/core/migrate/detector-rules.js +147 -0
- package/dist/src/core/migrate/detector.d.ts +16 -0
- package/dist/src/core/migrate/detector.js +105 -0
- package/dist/src/core/migrate/dispatcher.d.ts +19 -0
- package/dist/src/core/migrate/dispatcher.js +358 -0
- package/dist/src/core/migrate/doctor-checks.d.ts +19 -0
- package/dist/src/core/migrate/doctor-checks.js +304 -0
- package/dist/src/core/migrate/envelope.d.ts +25 -0
- package/dist/src/core/migrate/envelope.js +84 -0
- package/dist/src/core/migrate/executor.d.ts +33 -0
- package/dist/src/core/migrate/executor.js +102 -0
- package/dist/src/core/migrate/handshake.d.ts +17 -0
- package/dist/src/core/migrate/handshake.js +130 -0
- package/dist/src/core/migrate/migrator.d.ts +34 -0
- package/dist/src/core/migrate/migrator.js +302 -0
- package/dist/src/core/migrate/monorepo.d.ts +2 -0
- package/dist/src/core/migrate/monorepo.js +114 -0
- package/dist/src/core/migrate/policy-enforcer.d.ts +28 -0
- package/dist/src/core/migrate/policy-enforcer.js +111 -0
- package/dist/src/core/migrate/result-parser.d.ts +16 -0
- package/dist/src/core/migrate/result-parser.js +152 -0
- package/dist/src/core/migrate/schema-validator.d.ts +11 -0
- package/dist/src/core/migrate/schema-validator.js +103 -0
- package/dist/src/core/migrate/types.d.ts +49 -0
- package/dist/src/core/migrate/types.js +3 -0
- package/dist/src/core/phases/tests.js +19 -0
- package/package.json +5 -1
- package/presets/aliases.lock.json +20 -0
- package/presets/python/guardrail.config.yaml +11 -0
- package/presets/schemas/migrate.schema.json +134 -0
- package/skills/autopilot/SKILL.md +29 -9
- package/skills/migrate/skill.manifest.json +7 -0
- package/skills/migrate-none/SKILL.md +40 -0
- package/skills/migrate-none/skill.manifest.json +7 -0
- package/skills/migrate-supabase/SKILL.md +126 -0
- package/skills/migrate-supabase/skill.manifest.json +7 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/cli/migrate-doctor.ts
|
|
2
|
+
//
|
|
3
|
+
// CLI wrapper for migrate doctor checks (Task 7.2).
|
|
4
|
+
//
|
|
5
|
+
// runMigrateDoctor({ repoRoot, fix? }):
|
|
6
|
+
//
|
|
7
|
+
// fix = false (default, "plain doctor")
|
|
8
|
+
// - calls runAllChecks
|
|
9
|
+
// - returns the named results unchanged
|
|
10
|
+
// - NEVER writes to disk (asserted by golden-file test)
|
|
11
|
+
//
|
|
12
|
+
// fix = true ("doctor --fix")
|
|
13
|
+
// - runs the same checks
|
|
14
|
+
// - applies AUTO-FIXABLE mutations to .autopilot/stack.md:
|
|
15
|
+
// a) top-level `dev_command` → `migrate.envs.dev.command`
|
|
16
|
+
// b) missing `schema_version: 1`
|
|
17
|
+
// c) raw `migrate.skill` → stable ID (via resolveSkill)
|
|
18
|
+
// d) missing default policy keys backfilled (per skill shape)
|
|
19
|
+
// - writes the updated YAML, then re-runs the checks and returns
|
|
20
|
+
// both the post-fix results and the list of mutations performed
|
|
21
|
+
//
|
|
22
|
+
// Spec: docs/superpowers/specs/2026-04-29-migrate-skill-generalization-design.md
|
|
23
|
+
// (§ "claude-autopilot doctor")
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import * as yaml from 'js-yaml';
|
|
27
|
+
import { runAllChecks } from "../core/migrate/doctor-checks.js";
|
|
28
|
+
import { resolveSkill } from "../core/migrate/alias-resolver.js";
|
|
29
|
+
import { detectsLegacyMigrateSkill, migrateLegacySkill, } from "../core/migrate/migrator.js";
|
|
30
|
+
const DEFAULT_POLICY_GENERIC = {
|
|
31
|
+
allow_prod_in_ci: false,
|
|
32
|
+
require_clean_git: true,
|
|
33
|
+
require_manual_approval: true,
|
|
34
|
+
require_dry_run_first: false,
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_POLICY_SUPABASE = {
|
|
37
|
+
allow_prod_in_ci: false,
|
|
38
|
+
};
|
|
39
|
+
function stackPath(repoRoot) {
|
|
40
|
+
return path.join(repoRoot, '.autopilot', 'stack.md');
|
|
41
|
+
}
|
|
42
|
+
function applyAutoFixes(repoRoot) {
|
|
43
|
+
const sp = stackPath(repoRoot);
|
|
44
|
+
if (!fs.existsSync(sp)) {
|
|
45
|
+
return { mutations: [], wrote: false };
|
|
46
|
+
}
|
|
47
|
+
const raw = fs.readFileSync(sp, 'utf8');
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
const loaded = yaml.load(raw);
|
|
51
|
+
if (!loaded || typeof loaded !== 'object') {
|
|
52
|
+
return { mutations: [], wrote: false };
|
|
53
|
+
}
|
|
54
|
+
parsed = loaded;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { mutations: [], wrote: false };
|
|
58
|
+
}
|
|
59
|
+
const mutations = [];
|
|
60
|
+
// a) Migrate top-level dev_command → migrate.envs.dev.command
|
|
61
|
+
if ('dev_command' in parsed) {
|
|
62
|
+
const legacy = parsed.dev_command;
|
|
63
|
+
parsed.migrate = parsed.migrate ?? {};
|
|
64
|
+
parsed.migrate.envs = parsed.migrate.envs ?? {};
|
|
65
|
+
if (!parsed.migrate.envs.dev?.command) {
|
|
66
|
+
parsed.migrate.envs.dev = parsed.migrate.envs.dev ?? {};
|
|
67
|
+
parsed.migrate.envs.dev.command = legacy;
|
|
68
|
+
mutations.push('migrated top-level dev_command → migrate.envs.dev.command');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
mutations.push('removed redundant top-level dev_command (envs.dev.command already set)');
|
|
72
|
+
}
|
|
73
|
+
delete parsed.dev_command;
|
|
74
|
+
}
|
|
75
|
+
// b) Backfill schema_version
|
|
76
|
+
if (parsed.schema_version === undefined) {
|
|
77
|
+
parsed.schema_version = 1;
|
|
78
|
+
mutations.push('added schema_version: 1');
|
|
79
|
+
}
|
|
80
|
+
// c) Normalize raw skill → stable ID
|
|
81
|
+
const skill = parsed.migrate?.skill;
|
|
82
|
+
if (typeof skill === 'string') {
|
|
83
|
+
const res = resolveSkill(skill, { repoRoot });
|
|
84
|
+
if (res.ok && res.normalizedFromRaw && res.stableId !== skill) {
|
|
85
|
+
parsed.migrate.skill = res.stableId;
|
|
86
|
+
mutations.push(`normalized migrate.skill: "${skill}" → "${res.stableId}"`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// d) Backfill missing default policy keys based on resolved skill shape
|
|
90
|
+
if (parsed.migrate) {
|
|
91
|
+
const resolvedSkill = parsed.migrate.skill;
|
|
92
|
+
let defaults = null;
|
|
93
|
+
if (resolvedSkill === 'migrate@1')
|
|
94
|
+
defaults = DEFAULT_POLICY_GENERIC;
|
|
95
|
+
else if (resolvedSkill === 'migrate.supabase@1')
|
|
96
|
+
defaults = DEFAULT_POLICY_SUPABASE;
|
|
97
|
+
if (defaults) {
|
|
98
|
+
const policy = (parsed.migrate.policy ?? {});
|
|
99
|
+
const added = [];
|
|
100
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
101
|
+
if (!(k in policy)) {
|
|
102
|
+
policy[k] = v;
|
|
103
|
+
added.push(k);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (added.length > 0) {
|
|
107
|
+
parsed.migrate.policy = policy;
|
|
108
|
+
mutations.push(`backfilled default policy keys: ${added.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (mutations.length === 0) {
|
|
113
|
+
return { mutations, wrote: false };
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(sp, yaml.dump(parsed, { lineWidth: 120, noRefs: true }), 'utf8');
|
|
116
|
+
return { mutations, wrote: true };
|
|
117
|
+
}
|
|
118
|
+
function isoStampForReport() {
|
|
119
|
+
return new Date().toISOString().replace(/[.:]/g, '-');
|
|
120
|
+
}
|
|
121
|
+
function writeMigrationReport(repoRoot, report, context) {
|
|
122
|
+
const dir = path.join(repoRoot, '.autopilot');
|
|
123
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
const file = path.join(dir, `migration-report-${isoStampForReport()}.md`);
|
|
125
|
+
const lines = [
|
|
126
|
+
`# Legacy /migrate skill migration report`,
|
|
127
|
+
``,
|
|
128
|
+
`- generated: ${new Date().toISOString()}`,
|
|
129
|
+
`- migrated: ${context.migrated}`,
|
|
130
|
+
];
|
|
131
|
+
if (context.reason)
|
|
132
|
+
lines.push(`- reason: ${context.reason}`);
|
|
133
|
+
if (context.archivePath) {
|
|
134
|
+
lines.push(`- archive: ${path.relative(repoRoot, context.archivePath)}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push(``, `## Steps`, ``);
|
|
137
|
+
for (const step of report)
|
|
138
|
+
lines.push(`- ${step}`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
fs.writeFileSync(file, lines.join('\n'), 'utf8');
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
143
|
+
export async function runMigrateDoctor(opts) {
|
|
144
|
+
const repoRoot = path.resolve(opts.repoRoot);
|
|
145
|
+
const fix = opts.fix ?? false;
|
|
146
|
+
if (!fix) {
|
|
147
|
+
// Plain doctor: read-only.
|
|
148
|
+
const results = runAllChecks(repoRoot);
|
|
149
|
+
// Detect-only: surface legacy /migrate skill presence as a separate
|
|
150
|
+
// named check so callers can see it, but never write.
|
|
151
|
+
if (detectsLegacyMigrateSkill(repoRoot)) {
|
|
152
|
+
results.push({
|
|
153
|
+
name: 'legacyMigrateSkillAbsent',
|
|
154
|
+
result: {
|
|
155
|
+
ok: false,
|
|
156
|
+
message: 'skills/migrate/SKILL.md still has the legacy Supabase shape — run with --fix to migrate',
|
|
157
|
+
fixHint: 'claude-autopilot migrate doctor --fix',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { allOk: results.every(r => r.result.ok), results };
|
|
162
|
+
}
|
|
163
|
+
// --fix: apply auto-fixable mutations, then re-run checks.
|
|
164
|
+
const { mutations: yamlMutations } = applyAutoFixes(repoRoot);
|
|
165
|
+
const mutations = [...yamlMutations];
|
|
166
|
+
let migrationReportPath;
|
|
167
|
+
// Wire in the legacy /migrate skill migrator (Task 8.2).
|
|
168
|
+
if (detectsLegacyMigrateSkill(repoRoot)) {
|
|
169
|
+
const m = migrateLegacySkill({ repoRoot });
|
|
170
|
+
for (const step of m.report) {
|
|
171
|
+
mutations.push(`migrator: ${step}`);
|
|
172
|
+
}
|
|
173
|
+
if (m.migrated && m.reason) {
|
|
174
|
+
mutations.push(`migrator: completed (${m.reason})`);
|
|
175
|
+
}
|
|
176
|
+
migrationReportPath = writeMigrationReport(repoRoot, m.report, {
|
|
177
|
+
migrated: m.migrated,
|
|
178
|
+
reason: m.reason,
|
|
179
|
+
archivePath: m.archivePath,
|
|
180
|
+
});
|
|
181
|
+
mutations.push(`migrator: wrote migration report → ${path.relative(repoRoot, migrationReportPath)}`);
|
|
182
|
+
}
|
|
183
|
+
const results = runAllChecks(repoRoot);
|
|
184
|
+
return {
|
|
185
|
+
allOk: results.every(r => r.result.ok),
|
|
186
|
+
results,
|
|
187
|
+
mutations,
|
|
188
|
+
migrationReportPath,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=migrate-doctor.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ResolveOptions {
|
|
2
|
+
repoRoot: string;
|
|
3
|
+
/** Optional workspace path for monorepo lookup precedence (workspace
|
|
4
|
+
* aliases take precedence over repo-root aliases). */
|
|
5
|
+
workspace?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ResolveResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
stableId: string;
|
|
10
|
+
skillPath: string;
|
|
11
|
+
normalizedFromRaw: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reasonCode: 'aliases-file-missing' | 'aliases-file-invalid' | 'stable-id-unknown' | 'raw-alias-ambiguous' | 'path-escape' | 'skill-path-missing' | 'invalid-input';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function resolveSkill(input: string, opts: ResolveOptions): ResolveResult;
|
|
18
|
+
//# sourceMappingURL=alias-resolver.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/core/migrate/alias-resolver.ts
|
|
2
|
+
//
|
|
3
|
+
// Resolves stable skill IDs (e.g. "migrate@1") or raw aliases (e.g. "migrate")
|
|
4
|
+
// against presets/aliases.lock.json. Path escape is the CRITICAL security
|
|
5
|
+
// concern per Codex review:
|
|
6
|
+
// - resolved paths are realpath'd and verified to stay under <repo>/skills/
|
|
7
|
+
// or <repo>/node_modules/ (the trusted skill roots)
|
|
8
|
+
// - absolute paths in alias map are rejected
|
|
9
|
+
// - .. traversal in alias map is rejected
|
|
10
|
+
// - symlinks pointing outside trusted root are rejected (resolved + checked)
|
|
11
|
+
//
|
|
12
|
+
// Raw alias collisions are a hard error: ambiguous "thing" → require user to
|
|
13
|
+
// use exact stable ID.
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { TRUSTED_SKILL_ROOTS } from "./contract.js";
|
|
17
|
+
import { findPackageRoot } from "../../cli/_pkg-root.js";
|
|
18
|
+
function loadAliasMap(repoRoot) {
|
|
19
|
+
// Lookup precedence:
|
|
20
|
+
// 1. repoRoot/presets/aliases.lock.json — repo-local override (e.g. monorepo)
|
|
21
|
+
// 2. <package-root>/presets/aliases.lock.json — installed package
|
|
22
|
+
// findPackageRoot walks up from this module looking for the package.json
|
|
23
|
+
// declaring '@delegance/claude-autopilot', so it works under both source and
|
|
24
|
+
// compiled (dist/) layouts. Earlier code used __dirname + '../../..' which
|
|
25
|
+
// landed at <install>/dist/presets/ in the published tarball (presets/ ships
|
|
26
|
+
// at the package root, not under dist/).
|
|
27
|
+
const pkgRoot = findPackageRoot(import.meta.url);
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(repoRoot, 'presets', 'aliases.lock.json'),
|
|
30
|
+
...(pkgRoot ? [path.join(pkgRoot, 'presets', 'aliases.lock.json')] : []),
|
|
31
|
+
];
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
if (fs.existsSync(p)) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (typeof parsed?.schemaVersion === 'number' && Array.isArray(parsed.aliases)) {
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// continue to next candidate
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function isUnderTrustedRoot(absResolvedPath, repoRoot) {
|
|
49
|
+
let realRepoRoot;
|
|
50
|
+
try {
|
|
51
|
+
realRepoRoot = fs.realpathSync(repoRoot);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
for (const root of TRUSTED_SKILL_ROOTS) {
|
|
57
|
+
const trustedAbs = path.resolve(realRepoRoot, root);
|
|
58
|
+
const rel = path.relative(trustedAbs, absResolvedPath);
|
|
59
|
+
// rel must NOT start with '..' and must not be absolute (path traversal sentinels)
|
|
60
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Also accept the trusted root itself
|
|
64
|
+
if (absResolvedPath === trustedAbs) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function validatePathEscape(rawResolvesTo, repoRoot) {
|
|
71
|
+
// Reject absolute paths and .. traversal in the alias map *before* resolving
|
|
72
|
+
if (path.isAbsolute(rawResolvesTo)) {
|
|
73
|
+
return { ok: false, reason: 'absolute path in alias map' };
|
|
74
|
+
}
|
|
75
|
+
if (rawResolvesTo.split(/[/\\]/).some(seg => seg === '..')) {
|
|
76
|
+
return { ok: false, reason: '.. traversal in alias map' };
|
|
77
|
+
}
|
|
78
|
+
// Resolve via realpath (follows symlinks)
|
|
79
|
+
const candidate = path.resolve(repoRoot, rawResolvesTo);
|
|
80
|
+
let real;
|
|
81
|
+
try {
|
|
82
|
+
real = fs.realpathSync(candidate);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { ok: false, reason: `path does not exist: ${candidate}` };
|
|
86
|
+
}
|
|
87
|
+
if (!isUnderTrustedRoot(real, repoRoot)) {
|
|
88
|
+
return { ok: false, reason: `resolved path escapes trusted roots: ${real}` };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, resolved: real };
|
|
91
|
+
}
|
|
92
|
+
export function resolveSkill(input, opts) {
|
|
93
|
+
if (!input || typeof input !== 'string') {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reasonCode: 'invalid-input',
|
|
97
|
+
message: 'skill input must be a non-empty string',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Lookup precedence: workspace .autopilot/aliases.lock.json (if exists) > repo root presets/aliases.lock.json
|
|
101
|
+
const lookupRoot = opts.repoRoot;
|
|
102
|
+
const aliasMap = loadAliasMap(lookupRoot);
|
|
103
|
+
if (!aliasMap) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reasonCode: 'aliases-file-missing',
|
|
107
|
+
message: `no aliases.lock.json found under ${lookupRoot}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// 1. Exact stable ID match
|
|
111
|
+
const stableMatch = aliasMap.aliases.find(a => a.stableId === input);
|
|
112
|
+
if (stableMatch) {
|
|
113
|
+
return finalizeResolve(stableMatch, lookupRoot, /*normalizedFromRaw*/ false);
|
|
114
|
+
}
|
|
115
|
+
// 2. Raw alias normalization (with collision check)
|
|
116
|
+
const rawMatches = aliasMap.aliases.filter(a => a.rawAliases?.includes(input));
|
|
117
|
+
if (rawMatches.length > 1) {
|
|
118
|
+
const candidates = rawMatches.map(m => m.stableId).join(', ');
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
reasonCode: 'raw-alias-ambiguous',
|
|
122
|
+
message: `raw alias '${input}' maps to multiple stable IDs: ${candidates}. Use the exact stable ID in stack.md.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (rawMatches.length === 1) {
|
|
126
|
+
return finalizeResolve(rawMatches[0], lookupRoot, /*normalizedFromRaw*/ true);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reasonCode: 'stable-id-unknown',
|
|
131
|
+
message: `unknown skill '${input}'. Known stable IDs: ${aliasMap.aliases.map(a => a.stableId).join(', ')}. Run \`claude-autopilot doctor\` for help.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function finalizeResolve(entry, repoRoot, normalizedFromRaw) {
|
|
135
|
+
const check = validatePathEscape(entry.resolvesTo, repoRoot);
|
|
136
|
+
if (!check.ok) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reasonCode: 'path-escape',
|
|
140
|
+
message: `alias ${entry.stableId} resolvesTo path-escape rejected: ${check.reason}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
stableId: entry.stableId,
|
|
146
|
+
skillPath: check.resolved,
|
|
147
|
+
normalizedFromRaw,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=alias-resolver.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface AuditEvent {
|
|
2
|
+
seq: number;
|
|
3
|
+
ts: string;
|
|
4
|
+
invocationId: string;
|
|
5
|
+
event: string;
|
|
6
|
+
requested_skill: string;
|
|
7
|
+
resolved_skill: string;
|
|
8
|
+
skill_path: string;
|
|
9
|
+
envelope_contract_version: string;
|
|
10
|
+
skill_runtime_api_version: string;
|
|
11
|
+
envelope_hash: string;
|
|
12
|
+
policy_decisions: string[];
|
|
13
|
+
mode: 'apply' | 'dry-run' | 'doctor-fix';
|
|
14
|
+
actor: string;
|
|
15
|
+
ci_provider: string | null;
|
|
16
|
+
ci_run_id: string | null;
|
|
17
|
+
result_status: string;
|
|
18
|
+
duration_ms: number;
|
|
19
|
+
prev_hash: string | null;
|
|
20
|
+
}
|
|
21
|
+
export type AuditEventInput = Omit<AuditEvent, 'seq' | 'prev_hash' | 'ts'>;
|
|
22
|
+
export declare function appendAuditEvent(logPath: string, input: AuditEventInput): Promise<void>;
|
|
23
|
+
export declare function readEvents(logPath: string): AuditEvent[];
|
|
24
|
+
export interface VerifyResult {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
breakAtLine?: number;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function verifyChain(logPath: string): VerifyResult;
|
|
30
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/core/migrate/audit-log.ts
|
|
2
|
+
//
|
|
3
|
+
// JSONL audit log with monotonic seq + prev_hash chain. Concurrent writes
|
|
4
|
+
// serialized via proper-lockfile (advisory lock with retries + stale recovery).
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as crypto from 'node:crypto';
|
|
8
|
+
import lockfile from 'proper-lockfile';
|
|
9
|
+
function sha256(s) {
|
|
10
|
+
return 'sha256:' + crypto.createHash('sha256').update(s).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
function readLastLine(p) {
|
|
13
|
+
if (!fs.existsSync(p))
|
|
14
|
+
return null;
|
|
15
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
16
|
+
if (!raw.trim())
|
|
17
|
+
return null;
|
|
18
|
+
const lines = raw.trim().split('\n');
|
|
19
|
+
const last = lines[lines.length - 1];
|
|
20
|
+
const obj = JSON.parse(last);
|
|
21
|
+
return { seq: obj.seq, lineHash: sha256(last) };
|
|
22
|
+
}
|
|
23
|
+
export async function appendAuditEvent(logPath, input) {
|
|
24
|
+
const dir = path.dirname(logPath);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
if (!fs.existsSync(logPath))
|
|
27
|
+
fs.writeFileSync(logPath, '');
|
|
28
|
+
let release;
|
|
29
|
+
try {
|
|
30
|
+
release = await lockfile.lock(logPath, {
|
|
31
|
+
retries: { retries: 10, factor: 1.5, minTimeout: 50, maxTimeout: 500 },
|
|
32
|
+
stale: 5000,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// Fail closed: refuse to write a potentially-forked entry.
|
|
37
|
+
throw new Error(`audit-log: could not acquire lock on ${logPath}: ${err.message}. ` +
|
|
38
|
+
`Check for stale locks or filesystem issues. Audit chain not extended.`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const last = readLastLine(logPath);
|
|
42
|
+
const seq = (last?.seq ?? 0) + 1;
|
|
43
|
+
const prev_hash = last?.lineHash ?? null;
|
|
44
|
+
const event = {
|
|
45
|
+
...input,
|
|
46
|
+
seq,
|
|
47
|
+
prev_hash,
|
|
48
|
+
ts: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
// Append + fsync so chain is durable before lock release.
|
|
51
|
+
const handle = fs.openSync(logPath, 'a');
|
|
52
|
+
try {
|
|
53
|
+
fs.writeSync(handle, JSON.stringify(event) + '\n');
|
|
54
|
+
fs.fsyncSync(handle);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.closeSync(handle);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await release();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function readEvents(logPath) {
|
|
65
|
+
if (!fs.existsSync(logPath))
|
|
66
|
+
return [];
|
|
67
|
+
const raw = fs.readFileSync(logPath, 'utf8').trim();
|
|
68
|
+
if (!raw)
|
|
69
|
+
return [];
|
|
70
|
+
return raw.split('\n').map(line => JSON.parse(line));
|
|
71
|
+
}
|
|
72
|
+
export function verifyChain(logPath) {
|
|
73
|
+
const lines = (fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '').trim();
|
|
74
|
+
if (!lines)
|
|
75
|
+
return { valid: true };
|
|
76
|
+
const arr = lines.split('\n');
|
|
77
|
+
let expectedSeq = 1;
|
|
78
|
+
let expectedPrevHash = null;
|
|
79
|
+
for (let i = 0; i < arr.length; i++) {
|
|
80
|
+
const lineNo = i + 1;
|
|
81
|
+
const line = arr[i];
|
|
82
|
+
let obj;
|
|
83
|
+
try {
|
|
84
|
+
obj = JSON.parse(line);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { valid: false, breakAtLine: lineNo, reason: 'invalid-json' };
|
|
88
|
+
}
|
|
89
|
+
if (obj.seq !== expectedSeq) {
|
|
90
|
+
return { valid: false, breakAtLine: lineNo, reason: `seq mismatch: expected ${expectedSeq}, got ${obj.seq}` };
|
|
91
|
+
}
|
|
92
|
+
if (obj.prev_hash !== expectedPrevHash) {
|
|
93
|
+
return { valid: false, breakAtLine: lineNo, reason: 'prev_hash mismatch' };
|
|
94
|
+
}
|
|
95
|
+
expectedSeq += 1;
|
|
96
|
+
expectedPrevHash = sha256(line);
|
|
97
|
+
}
|
|
98
|
+
return { valid: true };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Wire format version for the envelope + result artifact. Skills must
|
|
2
|
+
* declare a compatible skill_runtime_api_version. */
|
|
3
|
+
export declare const ENVELOPE_CONTRACT_VERSION: "1.0";
|
|
4
|
+
/** Hard cap on result artifact size. Larger output rejected with
|
|
5
|
+
* reasonCode: 'result-too-large'. */
|
|
6
|
+
export declare const RESULT_ARTIFACT_MAX_BYTES = 1048576;
|
|
7
|
+
/** Stdout fallback marker prefix; nonce-bound. Format:
|
|
8
|
+
* @@AUTOPILOT_RESULT_BEGIN:<nonce>@@\n{...}\n@@AUTOPILOT_RESULT_END:<nonce>@@
|
|
9
|
+
* Disabled by default; opt-in via skill manifest stdoutFallback: true. */
|
|
10
|
+
export declare const STDOUT_MARKER_BEGIN_PREFIX = "@@AUTOPILOT_RESULT_BEGIN:";
|
|
11
|
+
export declare const STDOUT_MARKER_END_PREFIX = "@@AUTOPILOT_RESULT_END:";
|
|
12
|
+
export declare const STDOUT_MARKER_SUFFIX = "@@";
|
|
13
|
+
/** Reserved sideEffectsPerformed enum (v1). Skills cannot invent values;
|
|
14
|
+
* new entries land via package release. */
|
|
15
|
+
export declare const RESERVED_SIDE_EFFECTS: readonly ["types-regenerated", "migration-ledger-updated", "schema-cache-refreshed", "seed-data-applied", "snapshot-written", "no-side-effects"];
|
|
16
|
+
/** Shell metacharacters forbidden in CommandSpec args[] entries. The
|
|
17
|
+
* structured argv contract executes via spawn(shell:false), so these
|
|
18
|
+
* characters provide no benefit and are rejected at schema validation. */
|
|
19
|
+
export declare const SHELL_METACHARS: RegExp;
|
|
20
|
+
/** Trusted root prefixes for skill resolution. resolved skill paths must
|
|
21
|
+
* start with one of these (after realpath canonicalization) — prevents
|
|
22
|
+
* alias map entries from escaping the repo or installed package dir. */
|
|
23
|
+
export declare const TRUSTED_SKILL_ROOTS: readonly ["skills/", "node_modules/"];
|
|
24
|
+
/** Default temp directory permissions for per-invocation result artifact
|
|
25
|
+
* storage. 0700 = rwx for owner only. */
|
|
26
|
+
export declare const RESULT_TEMPDIR_MODE = 448;
|
|
27
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/core/migrate/contract.ts
|
|
2
|
+
/** Wire format version for the envelope + result artifact. Skills must
|
|
3
|
+
* declare a compatible skill_runtime_api_version. */
|
|
4
|
+
export const ENVELOPE_CONTRACT_VERSION = '1.0';
|
|
5
|
+
/** Hard cap on result artifact size. Larger output rejected with
|
|
6
|
+
* reasonCode: 'result-too-large'. */
|
|
7
|
+
export const RESULT_ARTIFACT_MAX_BYTES = 1_048_576;
|
|
8
|
+
/** Stdout fallback marker prefix; nonce-bound. Format:
|
|
9
|
+
* @@AUTOPILOT_RESULT_BEGIN:<nonce>@@\n{...}\n@@AUTOPILOT_RESULT_END:<nonce>@@
|
|
10
|
+
* Disabled by default; opt-in via skill manifest stdoutFallback: true. */
|
|
11
|
+
export const STDOUT_MARKER_BEGIN_PREFIX = '@@AUTOPILOT_RESULT_BEGIN:';
|
|
12
|
+
export const STDOUT_MARKER_END_PREFIX = '@@AUTOPILOT_RESULT_END:';
|
|
13
|
+
export const STDOUT_MARKER_SUFFIX = '@@';
|
|
14
|
+
/** Reserved sideEffectsPerformed enum (v1). Skills cannot invent values;
|
|
15
|
+
* new entries land via package release. */
|
|
16
|
+
export const RESERVED_SIDE_EFFECTS = [
|
|
17
|
+
'types-regenerated',
|
|
18
|
+
'migration-ledger-updated',
|
|
19
|
+
'schema-cache-refreshed',
|
|
20
|
+
'seed-data-applied',
|
|
21
|
+
'snapshot-written',
|
|
22
|
+
'no-side-effects',
|
|
23
|
+
];
|
|
24
|
+
/** Shell metacharacters forbidden in CommandSpec args[] entries. The
|
|
25
|
+
* structured argv contract executes via spawn(shell:false), so these
|
|
26
|
+
* characters provide no benefit and are rejected at schema validation. */
|
|
27
|
+
export const SHELL_METACHARS = /[|;&><`$()]/;
|
|
28
|
+
/** Trusted root prefixes for skill resolution. resolved skill paths must
|
|
29
|
+
* start with one of these (after realpath canonicalization) — prevents
|
|
30
|
+
* alias map entries from escaping the repo or installed package dir. */
|
|
31
|
+
export const TRUSTED_SKILL_ROOTS = ['skills/', 'node_modules/'];
|
|
32
|
+
/** Default temp directory permissions for per-invocation result artifact
|
|
33
|
+
* storage. 0700 = rwx for owner only. */
|
|
34
|
+
export const RESULT_TEMPDIR_MODE = 0o700;
|
|
35
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CommandSpec } from './types.ts';
|
|
2
|
+
export type Confidence = 'high' | 'medium' | 'low';
|
|
3
|
+
export interface DetectionRule {
|
|
4
|
+
name: string;
|
|
5
|
+
stack: string;
|
|
6
|
+
confidence: Confidence;
|
|
7
|
+
/** All entries must exist (relative to project_root) for a match. */
|
|
8
|
+
requireAll: string[];
|
|
9
|
+
/** At least one entry must exist (relative to project_root). Optional. */
|
|
10
|
+
requireAny?: string[];
|
|
11
|
+
/** Patterns to glob for; at least one match required. Optional. */
|
|
12
|
+
requireGlob?: string[];
|
|
13
|
+
/** Path that, if present, disqualifies the rule (e.g. supabase-bare excluded if data/deltas/ present). */
|
|
14
|
+
excludeIf?: string[];
|
|
15
|
+
/** A file's content must contain this regex (e.g. Gemfile contains rails). */
|
|
16
|
+
contentMatches?: {
|
|
17
|
+
file: string;
|
|
18
|
+
pattern: RegExp;
|
|
19
|
+
};
|
|
20
|
+
defaultSkill: string;
|
|
21
|
+
defaultCommand?: CommandSpec;
|
|
22
|
+
/** When confidence is low/medium, prompt user before auto-selecting. */
|
|
23
|
+
promptOnSelect: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare const DETECTION_RULES: DetectionRule[];
|
|
26
|
+
//# sourceMappingURL=detector-rules.d.ts.map
|