@bvdm/delano 0.1.7 → 0.1.8
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/.delano/README.md +7 -0
- package/.delano/viewer/README.md +19 -0
- package/.delano/viewer/public/app.js +818 -0
- package/.delano/viewer/public/explorer.svg +3 -0
- package/.delano/viewer/public/index.html +21 -0
- package/.delano/viewer/public/markdown.svg +6 -0
- package/.delano/viewer/public/styles.css +1042 -0
- package/.delano/viewer/public/vscode.svg +24 -0
- package/.delano/viewer/server.js +389 -0
- package/HANDBOOK.md +65 -45
- package/README.md +10 -2
- package/assets/install-manifest.json +112 -35
- package/assets/payload/.agents/README.md +31 -6
- package/assets/payload/.agents/adapters/claude/README.md +22 -3
- package/assets/payload/.agents/adapters/codex/README.md +22 -3
- package/assets/payload/.agents/adapters/opencode/README.md +22 -3
- package/assets/payload/.agents/adapters/pi/README.md +22 -3
- package/assets/payload/.agents/common/log-safety.js +55 -0
- package/assets/payload/.agents/eval-fixtures/skill-output/invalid/missing-evidence/output.json +6 -0
- package/assets/payload/.agents/eval-fixtures/skill-output/valid/summary/output.json +7 -0
- package/assets/payload/.agents/fixtures/github/status-snapshot.json +6 -0
- package/assets/payload/.agents/fixtures/linear/issue-snapshot.json +6 -0
- package/assets/payload/.agents/hooks/bash-worktree-fix.sh +2 -1
- package/assets/payload/.agents/hooks/post-tool-logger.js +2 -1
- package/assets/payload/.agents/hooks/user-prompt-logger.js +17 -1
- package/assets/payload/.agents/logs/delivery-metrics.md +22 -0
- package/assets/payload/.agents/logs/schema.md +20 -1
- package/assets/payload/.agents/rules/delivery-modes.md +17 -0
- package/assets/payload/.agents/schemas/README.md +22 -0
- package/assets/payload/.agents/schemas/artifact-scope.json +237 -0
- package/assets/payload/.agents/schemas/artifacts/context.schema.json +11 -0
- package/assets/payload/.agents/schemas/artifacts/decision_log.schema.json +12 -0
- package/assets/payload/.agents/schemas/artifacts/evidence.schema.json +17 -0
- package/assets/payload/.agents/schemas/artifacts/plan.schema.json +83 -0
- package/assets/payload/.agents/schemas/artifacts/spec.schema.json +101 -0
- package/assets/payload/.agents/schemas/artifacts/task.schema.json +121 -0
- package/assets/payload/.agents/schemas/artifacts/update.schema.json +12 -0
- package/assets/payload/.agents/schemas/artifacts/workstream.schema.json +66 -0
- package/assets/payload/.agents/schemas/evidence-map.json +53 -0
- package/assets/payload/.agents/schemas/learning/closeout-learning-proposal.schema.json +20 -0
- package/assets/payload/.agents/schemas/learning/delivery-metric-event.schema.json +21 -0
- package/assets/payload/.agents/schemas/leases/lease.schema.json +39 -0
- package/assets/payload/.agents/schemas/metrics/delivery-event.schema.json +29 -0
- package/assets/payload/.agents/schemas/metrics/delivery-events.schema.json +49 -0
- package/assets/payload/.agents/schemas/operating-modes.json +42 -0
- package/assets/payload/.agents/schemas/status-transitions.json +31 -0
- package/assets/payload/.agents/schemas/sync/drift-report.schema.json +25 -0
- package/assets/payload/.agents/schemas/sync/drift-taxonomy.json +38 -0
- package/assets/payload/.agents/schemas/sync/sync-map.schema.json +39 -0
- package/assets/payload/.agents/scripts/README.md +1 -0
- package/assets/payload/.agents/scripts/audit-context-files.mjs +54 -0
- package/assets/payload/.agents/scripts/audit-context-scoring.mjs +14 -0
- package/assets/payload/.agents/scripts/build-drift-report.mjs +133 -0
- package/assets/payload/.agents/scripts/check-artifact-schemas.mjs +116 -0
- package/assets/payload/.agents/scripts/check-closeout-learning-proposals.mjs +23 -0
- package/assets/payload/.agents/scripts/check-context-audit.mjs +61 -0
- package/assets/payload/.agents/scripts/check-delivery-metric-events.mjs +35 -0
- package/assets/payload/.agents/scripts/check-delivery-metrics.mjs +52 -0
- package/assets/payload/.agents/scripts/check-evidence-map.mjs +143 -0
- package/assets/payload/.agents/scripts/check-github-status-inspection.mjs +93 -0
- package/assets/payload/.agents/scripts/check-github-sync.mjs +159 -0
- package/assets/payload/.agents/scripts/check-handoff-summaries.mjs +57 -0
- package/assets/payload/.agents/scripts/check-lease-conflicts.mjs +24 -0
- package/assets/payload/.agents/scripts/check-lease-contracts.mjs +17 -0
- package/assets/payload/.agents/scripts/check-linear-issue-inspection.mjs +63 -0
- package/assets/payload/.agents/scripts/check-local-sync-map.mjs +151 -0
- package/assets/payload/.agents/scripts/check-log-safety.sh +62 -0
- package/assets/payload/.agents/scripts/check-operating-modes.mjs +99 -0
- package/assets/payload/.agents/scripts/check-path-standards.sh +1 -1
- package/assets/payload/.agents/scripts/check-skill-output-evals.mjs +13 -0
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +169 -0
- package/assets/payload/.agents/scripts/check-strict-fixtures.mjs +140 -0
- package/assets/payload/.agents/scripts/check-sync-schemas.mjs +52 -0
- package/assets/payload/.agents/scripts/check-text-safety.mjs +158 -0
- package/assets/payload/.agents/scripts/check-worktree-health.mjs +100 -0
- package/assets/payload/.agents/scripts/fix-path-standards.sh +1 -1
- package/assets/payload/.agents/scripts/inspect-github-sync.mjs +108 -0
- package/assets/payload/.agents/scripts/lease-manager.mjs +88 -0
- package/assets/payload/.agents/scripts/log-event.js +3 -0
- package/assets/payload/.agents/scripts/plan-sync-repairs.mjs +66 -0
- package/assets/payload/.agents/scripts/pm/validate.sh +656 -2
- package/assets/payload/.agents/scripts/propose-closeout-learning.mjs +20 -0
- package/assets/payload/.agents/scripts/read-local-sync-map.mjs +135 -0
- package/assets/payload/.agents/scripts/select-next-task.mjs +22 -0
- package/assets/payload/.agents/scripts/summarize-project-metrics.mjs +15 -0
- package/assets/payload/.agents/skills/closeout-skill/SKILL.md +3 -0
- package/assets/payload/.agents/skills/closeout-skill/references/runbook.md +5 -2
- package/assets/payload/.agents/skills/closeout-skill/templates/closure-checklist.md +2 -0
- package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposal.md +21 -0
- package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposals.md +25 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/dependency.md +18 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/task.md +24 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/invalid-transition/task.md +20 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/missing-evidence/task.md +27 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/path-leak/task.md +27 -0
- package/assets/payload/.agents/validation-fixtures/strict/invalid/stale-context/context.md +9 -0
- package/assets/payload/.agents/validation-fixtures/strict/manifest.json +11 -0
- package/assets/payload/.agents/validation-fixtures/strict/valid/minimal-project/task.md +27 -0
- package/assets/payload/.delano/viewer/README.md +19 -0
- package/assets/payload/.delano/viewer/public/app.js +818 -0
- package/assets/payload/.delano/viewer/public/explorer.svg +3 -0
- package/assets/payload/.delano/viewer/public/index.html +21 -0
- package/assets/payload/.delano/viewer/public/markdown.svg +6 -0
- package/assets/payload/.delano/viewer/public/styles.css +1042 -0
- package/assets/payload/.delano/viewer/public/vscode.svg +24 -0
- package/assets/payload/.delano/viewer/server.js +389 -0
- package/assets/payload/.project/templates/plan.md +1 -1
- package/assets/payload/.project/templates/spec.md +1 -1
- package/assets/payload/.project/templates/task.md +1 -0
- package/assets/payload/HANDBOOK.md +65 -45
- package/package.json +31 -2
- package/src/cli/commands/viewer.js +81 -0
- package/src/cli/index.js +8 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const repoRoot = resolveRepoRoot(__dirname);
|
|
8
|
+
const errors = [];
|
|
9
|
+
const taxonomyPath = path.join(repoRoot, ".agents", "schemas", "sync", "drift-taxonomy.json");
|
|
10
|
+
const syncMapSchemaPath = path.join(repoRoot, ".agents", "schemas", "sync", "sync-map.schema.json");
|
|
11
|
+
const taxonomy = readJson(taxonomyPath, "drift taxonomy");
|
|
12
|
+
const syncMapSchema = readJson(syncMapSchemaPath, "sync map schema");
|
|
13
|
+
|
|
14
|
+
const requiredDrifts = ["mapping-drift", "status-drift", "dependency-drift", "orphan-drift", "repair-recommendation"];
|
|
15
|
+
if (taxonomy.schema_version !== 1) errors.push("drift taxonomy schema_version must be 1.");
|
|
16
|
+
const driftTypes = Array.isArray(taxonomy.drift_types) ? taxonomy.drift_types : [];
|
|
17
|
+
for (const id of requiredDrifts) {
|
|
18
|
+
const drift = driftTypes.find((entry) => entry.id === id);
|
|
19
|
+
if (!drift) {
|
|
20
|
+
errors.push(`drift taxonomy missing type: ${id}`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!drift.description || !Array.isArray(drift.severity) || drift.severity.length === 0 || !drift.repair_posture) {
|
|
24
|
+
errors.push(`drift type ${id} must define description, severity, and repair_posture.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (syncMapSchema.type !== "object") errors.push("sync map schema must be an object schema.");
|
|
29
|
+
for (const field of ["schema_version", "projects"]) {
|
|
30
|
+
if (!syncMapSchema.required?.includes(field)) errors.push(`sync map schema must require ${field}.`);
|
|
31
|
+
}
|
|
32
|
+
const projectProperties = syncMapSchema.properties?.projects?.items?.properties || {};
|
|
33
|
+
for (const field of ["slug", "local_path", "linear_project_id", "github_repo", "tasks"]) {
|
|
34
|
+
if (!projectProperties[field]) errors.push(`sync map project schema missing ${field}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (errors.length > 0) {
|
|
38
|
+
console.error("Sync schema check failed:");
|
|
39
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log("Sync schema check passed for drift taxonomy and sync map schema.");
|
|
43
|
+
|
|
44
|
+
function readJson(filePath, label) {
|
|
45
|
+
try { return JSON.parse(readFileSync(filePath, "utf8")); }
|
|
46
|
+
catch (error) { errors.push(`Could not read ${label}: ${error.message}`); return {}; }
|
|
47
|
+
}
|
|
48
|
+
function resolveRepoRoot(startDir) {
|
|
49
|
+
const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
|
|
50
|
+
for (const candidate of candidates) if (existsSync(path.join(candidate, ".agents"))) return candidate;
|
|
51
|
+
return path.resolve(startDir, "..");
|
|
52
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
9
|
+
|
|
10
|
+
const bidiControls = new Map([
|
|
11
|
+
[0x200E, "LEFT-TO-RIGHT MARK"],
|
|
12
|
+
[0x200F, "RIGHT-TO-LEFT MARK"],
|
|
13
|
+
[0x202A, "LEFT-TO-RIGHT EMBEDDING"],
|
|
14
|
+
[0x202B, "RIGHT-TO-LEFT EMBEDDING"],
|
|
15
|
+
[0x202C, "POP DIRECTIONAL FORMATTING"],
|
|
16
|
+
[0x202D, "LEFT-TO-RIGHT OVERRIDE"],
|
|
17
|
+
[0x202E, "RIGHT-TO-LEFT OVERRIDE"],
|
|
18
|
+
[0x2066, "LEFT-TO-RIGHT ISOLATE"],
|
|
19
|
+
[0x2067, "RIGHT-TO-LEFT ISOLATE"],
|
|
20
|
+
[0x2068, "FIRST STRONG ISOLATE"],
|
|
21
|
+
[0x2069, "POP DIRECTIONAL ISOLATE"]
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const binaryExtensions = new Set([
|
|
25
|
+
".bmp",
|
|
26
|
+
".gif",
|
|
27
|
+
".ico",
|
|
28
|
+
".jpeg",
|
|
29
|
+
".jpg",
|
|
30
|
+
".pdf",
|
|
31
|
+
".png",
|
|
32
|
+
".tgz",
|
|
33
|
+
".ttf",
|
|
34
|
+
".webp",
|
|
35
|
+
".woff",
|
|
36
|
+
".woff2",
|
|
37
|
+
".zip"
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const files = resolveFiles(args);
|
|
42
|
+
const findings = [];
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const absolutePath = path.resolve(repoRoot, file);
|
|
46
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (binaryExtensions.has(path.extname(absolutePath).toLowerCase())) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const buffer = readFileSync(absolutePath);
|
|
55
|
+
if (isProbablyBinary(buffer)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
inspectText(absolutePath, buffer.toString("utf8"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (findings.length > 0) {
|
|
63
|
+
console.error("Text safety check failed:");
|
|
64
|
+
for (const finding of findings) {
|
|
65
|
+
console.error(`- ${finding.file}:${finding.line}:${finding.column} contains ${finding.code} (${finding.name})`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Text safety check passed for ${files.length} tracked file(s).`);
|
|
71
|
+
|
|
72
|
+
function resolveFiles(rawArgs) {
|
|
73
|
+
if (rawArgs.length === 0) {
|
|
74
|
+
return gitTrackedFiles();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const selectedFiles = [];
|
|
78
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
79
|
+
const arg = rawArgs[index];
|
|
80
|
+
if (arg === "--file") {
|
|
81
|
+
const value = rawArgs[index + 1];
|
|
82
|
+
if (!value) {
|
|
83
|
+
throw new Error("--file requires a path");
|
|
84
|
+
}
|
|
85
|
+
selectedFiles.push(value);
|
|
86
|
+
index += 1;
|
|
87
|
+
} else {
|
|
88
|
+
selectedFiles.push(arg);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return selectedFiles;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function gitTrackedFiles() {
|
|
96
|
+
const output = execFileSync("git", ["ls-files", "-z"], {
|
|
97
|
+
cwd: repoRoot,
|
|
98
|
+
encoding: "buffer"
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return output
|
|
102
|
+
.toString("utf8")
|
|
103
|
+
.split("\0")
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isProbablyBinary(buffer) {
|
|
108
|
+
const sampleLength = Math.min(buffer.length, 8000);
|
|
109
|
+
for (let index = 0; index < sampleLength; index += 1) {
|
|
110
|
+
if (buffer[index] === 0) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function inspectText(absolutePath, text) {
|
|
118
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
119
|
+
const codePoint = text.codePointAt(index);
|
|
120
|
+
if (!bidiControls.has(codePoint)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const position = locate(text, index);
|
|
125
|
+
findings.push({
|
|
126
|
+
file: displayPath(absolutePath),
|
|
127
|
+
line: position.line,
|
|
128
|
+
column: position.column,
|
|
129
|
+
code: `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
130
|
+
name: bidiControls.get(codePoint)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function locate(text, index) {
|
|
136
|
+
let line = 1;
|
|
137
|
+
let lineStart = 0;
|
|
138
|
+
|
|
139
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
140
|
+
if (text[cursor] === "\n") {
|
|
141
|
+
line += 1;
|
|
142
|
+
lineStart = cursor + 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
line,
|
|
148
|
+
column: index - lineStart + 1
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function displayPath(absolutePath) {
|
|
153
|
+
const relativePath = path.relative(repoRoot, absolutePath).replace(/\\/g, "/");
|
|
154
|
+
if (!path.isAbsolute(relativePath) && !relativePath.startsWith("..") && relativePath !== "") {
|
|
155
|
+
return relativePath;
|
|
156
|
+
}
|
|
157
|
+
return path.basename(absolutePath);
|
|
158
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const riskySharedPrefixes = [
|
|
5
|
+
".agents/scripts/",
|
|
6
|
+
".claude/scripts/",
|
|
7
|
+
"scripts/",
|
|
8
|
+
"package.json",
|
|
9
|
+
"assets/install-manifest.json",
|
|
10
|
+
"assets/payload/"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const status = git(["status", "--porcelain=v1"]);
|
|
14
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
15
|
+
const upstream = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
16
|
+
const worktreeList = git(["worktree", "list", "--porcelain"]);
|
|
17
|
+
const worktreePrune = git(["worktree", "prune", "--dry-run", "--verbose"]);
|
|
18
|
+
|
|
19
|
+
const issues = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
if (status.status !== 0) issues.push("git status failed");
|
|
22
|
+
const branchName = branch.stdout.trim();
|
|
23
|
+
if (branch.status !== 0 || !branchName) issues.push("branch could not be resolved");
|
|
24
|
+
else if (branchName === "HEAD") warnings.push("branch is detached");
|
|
25
|
+
if (worktreeList.status !== 0) issues.push("git worktree list failed");
|
|
26
|
+
if (worktreePrune.status !== 0) warnings.push("git worktree prune dry-run failed");
|
|
27
|
+
|
|
28
|
+
const dirtyFiles = parseStatus(status.stdout);
|
|
29
|
+
const riskySharedFiles = dirtyFiles.filter((file) => riskySharedPrefixes.some((prefix) => file.path === prefix || file.path.startsWith(prefix)));
|
|
30
|
+
const worktrees = parseWorktrees(worktreeList.stdout);
|
|
31
|
+
const staleWorktrees = parseStaleWorktrees(worktreePrune.stdout);
|
|
32
|
+
|
|
33
|
+
for (const file of riskySharedFiles) warnings.push(`risky shared file is dirty: ${file.path}`);
|
|
34
|
+
for (const stale of staleWorktrees) warnings.push(`stale worktree candidate: ${stale}`);
|
|
35
|
+
|
|
36
|
+
const result = {
|
|
37
|
+
schema_version: 1,
|
|
38
|
+
branch: branchName || "unknown",
|
|
39
|
+
upstream: upstream.status === 0 ? upstream.stdout.trim() : "",
|
|
40
|
+
dirty: dirtyFiles.length > 0,
|
|
41
|
+
dirty_files: dirtyFiles,
|
|
42
|
+
risky_shared_files: riskySharedFiles,
|
|
43
|
+
worktrees,
|
|
44
|
+
stale_worktrees: staleWorktrees,
|
|
45
|
+
issues,
|
|
46
|
+
warnings
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (process.argv.includes("--json")) {
|
|
50
|
+
console.log(JSON.stringify(result, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`Worktree health: ${issues.length ? "issues" : "ok"}; dirty=${result.dirty}; branch=${result.branch}; risky=${riskySharedFiles.length}; stale=${staleWorktrees.length}.`);
|
|
53
|
+
}
|
|
54
|
+
if (issues.length) process.exit(1);
|
|
55
|
+
|
|
56
|
+
function git(args) {
|
|
57
|
+
return spawnSync("git", args, { encoding: "utf8" });
|
|
58
|
+
}
|
|
59
|
+
function parseStatus(text) {
|
|
60
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
61
|
+
const statusCode = line.slice(0, 2);
|
|
62
|
+
const rawPath = line.slice(3).trim();
|
|
63
|
+
const filePath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() : rawPath;
|
|
64
|
+
return { status: statusCode.trim() || "??", path: normalizePath(filePath) };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function parseWorktrees(text) {
|
|
68
|
+
const entries = [];
|
|
69
|
+
let current = null;
|
|
70
|
+
for (const line of text.split("\n")) {
|
|
71
|
+
if (!line.trim()) {
|
|
72
|
+
if (current) entries.push(current);
|
|
73
|
+
current = null;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const [key, ...rest] = line.split(" ");
|
|
77
|
+
const value = rest.join(" ");
|
|
78
|
+
if (key === "worktree") current = { path: safeWorktreePath(value), branch: "", head: "", bare: false, detached: false };
|
|
79
|
+
else if (!current) continue;
|
|
80
|
+
else if (key === "HEAD") current.head = value;
|
|
81
|
+
else if (key === "branch") current.branch = value.replace(/^refs\/heads\//, "");
|
|
82
|
+
else if (key === "bare") current.bare = true;
|
|
83
|
+
else if (key === "detached") current.detached = true;
|
|
84
|
+
}
|
|
85
|
+
if (current) entries.push(current);
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
function parseStaleWorktrees(text) {
|
|
89
|
+
return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => normalizePath(line.replace(/^Removing worktrees\//, "worktrees/")));
|
|
90
|
+
}
|
|
91
|
+
function safeWorktreePath(value) {
|
|
92
|
+
const absolute = path.resolve(String(value || ""));
|
|
93
|
+
const cwd = path.resolve(process.cwd());
|
|
94
|
+
if (absolute === cwd) return ".";
|
|
95
|
+
if (absolute.startsWith(`${cwd}${path.sep}`)) return normalizePath(path.relative(cwd, absolute));
|
|
96
|
+
return `external:${path.basename(absolute)}`;
|
|
97
|
+
}
|
|
98
|
+
function normalizePath(value) {
|
|
99
|
+
return String(value || "").replaceAll(path.sep, "/");
|
|
100
|
+
}
|
|
@@ -9,6 +9,6 @@ fi
|
|
|
9
9
|
|
|
10
10
|
for file in "$@"; do
|
|
11
11
|
[[ -f "$file" ]] || { echo "Skip missing file: $file"; continue; }
|
|
12
|
-
perl -0777 -i -pe 's#/home/[^\s)]+#<ABS_PATH>#g; s#/Users/[^\s)]+#<ABS_PATH>#g; s#[A-Za-z]:\\[^\s)]+#<ABS_PATH>#g' "$file"
|
|
12
|
+
perl -0777 -i -pe 's#/home/[^\s)]+#<ABS_PATH>#g; s#/Users/[^\s)]+#<ABS_PATH>#g; s#/mnt/[A-Za-z]/[^\s)]+#<ABS_PATH>#g; s#[A-Za-z]:\\[^\s)]+#<ABS_PATH>#g' "$file"
|
|
13
13
|
echo "Normalized paths in: $file"
|
|
14
14
|
done
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readLocalSyncMap } from "./read-local-sync-map.mjs";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const repoRoot = resolveRepoRoot(__dirname);
|
|
9
|
+
const jsonMode = process.argv.includes("--json");
|
|
10
|
+
const errors = [];
|
|
11
|
+
const report = inspectGithubSync(repoRoot);
|
|
12
|
+
|
|
13
|
+
if (jsonMode) {
|
|
14
|
+
console.log(JSON.stringify(report, null, 2));
|
|
15
|
+
} else {
|
|
16
|
+
console.log(`GitHub sync inspection passed for ${report.projects.length} projects, ${report.summary.issue_refs} issue refs, and ${report.summary.pr_refs} PR refs.`);
|
|
17
|
+
for (const drift of report.drift) console.log(`- ${drift.severity}: ${drift.summary}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (errors.length > 0) {
|
|
21
|
+
if (!jsonMode) {
|
|
22
|
+
console.error("GitHub sync inspection failed:");
|
|
23
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
24
|
+
}
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function inspectGithubSync(root = repoRoot) {
|
|
29
|
+
const syncMap = readLocalSyncMap(root);
|
|
30
|
+
const fallbackRepo = normalizeGitHubRepo(readGitRemote(root));
|
|
31
|
+
const projects = [];
|
|
32
|
+
const drift = [];
|
|
33
|
+
let issueRefs = 0;
|
|
34
|
+
let prRefs = 0;
|
|
35
|
+
|
|
36
|
+
for (const project of syncMap.projects) {
|
|
37
|
+
const githubRepo = normalizeGitHubRepo(project.github_repo) || fallbackRepo || "";
|
|
38
|
+
const inspectedTasks = [];
|
|
39
|
+
for (const task of project.tasks) {
|
|
40
|
+
const issue = normalizeGitHubRef(task.github_issue, githubRepo, "issue");
|
|
41
|
+
const pr = normalizeGitHubRef(task.github_pr, githubRepo, "pull_request");
|
|
42
|
+
if (issue) issueRefs += 1;
|
|
43
|
+
if (pr) prRefs += 1;
|
|
44
|
+
if ((task.github_issue || task.github_pr) && !githubRepo) {
|
|
45
|
+
drift.push({
|
|
46
|
+
drift_type: "mapping-drift",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
target: `${project.slug}/${task.local_id}`,
|
|
49
|
+
summary: "Task has GitHub references but no project github_repo or origin GitHub remote.",
|
|
50
|
+
proposed_action: "Add github_repo to the sync registry or project contract before remote inspection.",
|
|
51
|
+
apply_posture: "dry-run-plan-first"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
inspectedTasks.push({
|
|
55
|
+
local_id: task.local_id,
|
|
56
|
+
status: task.status,
|
|
57
|
+
issue,
|
|
58
|
+
pull_request: pr
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
projects.push({ slug: project.slug, github_repo: githubRepo, tasks: inspectedTasks });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
schema_version: 1,
|
|
66
|
+
mode: "local-dry-run",
|
|
67
|
+
source: "local-task-metadata-and-git-remote",
|
|
68
|
+
summary: { issue_refs: issueRefs, pr_refs: prRefs, drift_count: drift.length },
|
|
69
|
+
projects,
|
|
70
|
+
drift
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeGitHubRef(value, fallbackRepo, kind) {
|
|
75
|
+
const raw = String(value || "").trim();
|
|
76
|
+
if (!raw) return null;
|
|
77
|
+
const numberMatch = raw.match(/^#?([0-9]+)$/);
|
|
78
|
+
if (numberMatch) return { kind, repo: fallbackRepo || "", number: Number(numberMatch[1]), url: fallbackRepo ? `https://github.com/${fallbackRepo}/${kind === "pull_request" ? "pull" : "issues"}/${numberMatch[1]}` : "" };
|
|
79
|
+
const urlMatch = raw.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(issues|pull)\/([0-9]+)\/?$/);
|
|
80
|
+
if (urlMatch) return { kind: urlMatch[2] === "pull" ? "pull_request" : "issue", repo: normalizeGitHubRepo(urlMatch[1]), number: Number(urlMatch[3]), url: raw.replace(/\/$/, "") };
|
|
81
|
+
errors.push(`Invalid GitHub ${kind} reference: ${raw}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeGitHubRepo(value) {
|
|
86
|
+
const raw = String(value || "").trim();
|
|
87
|
+
if (!raw) return "";
|
|
88
|
+
const ssh = raw.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
89
|
+
if (ssh) return ssh[1].replace(/\.git$/, "");
|
|
90
|
+
const https = raw.match(/^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?\/?$/);
|
|
91
|
+
if (https) return https[1].replace(/\.git$/, "");
|
|
92
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(raw)) return raw.replace(/\.git$/, "");
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readGitRemote(root) {
|
|
97
|
+
const configPath = path.join(root, ".git", "config");
|
|
98
|
+
if (!existsSync(configPath)) return "";
|
|
99
|
+
const text = readFileSync(configPath, "utf8");
|
|
100
|
+
const match = text.match(/\[remote "origin"\][\s\S]*?url = (.+)/);
|
|
101
|
+
return match ? match[1].trim() : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveRepoRoot(startDir) {
|
|
105
|
+
const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
|
|
106
|
+
for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
|
|
107
|
+
return path.resolve(startDir, "..");
|
|
108
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const repoRoot = resolveRepoRoot(__dirname);
|
|
9
|
+
const command = process.argv[2] || "self-test";
|
|
10
|
+
const statePath = readOption("--state") || path.join(repoRoot, ".agents", "leases", "active-leases.json");
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const result = run(command, statePath);
|
|
14
|
+
if (process.argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
|
|
15
|
+
else console.log(result.message);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(error.message);
|
|
18
|
+
process.exit(error.exitCode || 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function run(command, statePath) {
|
|
22
|
+
if (command === "self-test") return selfTest();
|
|
23
|
+
if (command === "list" || command === "inspect") return { message: `Lease inspection found ${readState(statePath).leases.length} lease(s).`, leases: readState(statePath).leases };
|
|
24
|
+
if (command === "acquire") return acquire(statePath, readRequired("--owner"), readRequired("--project"), readRequired("--task"), readList("--zone"), readOption("--mode") || "shared", Number(readOption("--ttl-minutes") || 120));
|
|
25
|
+
if (command === "release") return release(statePath, readRequired("--lease-id"), readOption("--reason") || "released by owner", readOption("--handoff") || "");
|
|
26
|
+
throw Object.assign(new Error(`Unknown lease command: ${command}`), { exitCode: 1 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function acquire(statePath, owner, project, taskId, zones, mode, ttlMinutes) {
|
|
30
|
+
if (!zones.length) throw Object.assign(new Error("At least one --zone is required."), { exitCode: 1 });
|
|
31
|
+
if (!["shared", "exclusive"].includes(mode)) throw Object.assign(new Error("--mode must be shared or exclusive."), { exitCode: 1 });
|
|
32
|
+
const state = readState(statePath);
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const lease = {
|
|
35
|
+
schema_version: 1,
|
|
36
|
+
lease_id: `lease-${now.toISOString().replace(/[:.]/g, "-")}-${taskId.toLowerCase()}`,
|
|
37
|
+
owner,
|
|
38
|
+
project,
|
|
39
|
+
task_id: taskId,
|
|
40
|
+
status: "active",
|
|
41
|
+
mode,
|
|
42
|
+
paths: zones,
|
|
43
|
+
conflict_zones: zones,
|
|
44
|
+
created_at: now.toISOString(),
|
|
45
|
+
acquired_at: now.toISOString(),
|
|
46
|
+
expires_at: new Date(now.getTime() + ttlMinutes * 60_000).toISOString()
|
|
47
|
+
};
|
|
48
|
+
state.leases.push(lease);
|
|
49
|
+
writeState(statePath, state);
|
|
50
|
+
return { message: `Acquired ${lease.lease_id} for ${project}/${taskId}.`, lease };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function release(statePath, leaseId, reason, handoff) {
|
|
54
|
+
const state = readState(statePath);
|
|
55
|
+
const lease = state.leases.find((item) => item.lease_id === leaseId);
|
|
56
|
+
if (!lease) throw Object.assign(new Error(`Lease not found: ${leaseId}`), { exitCode: 1 });
|
|
57
|
+
if (!handoff.trim()) {
|
|
58
|
+
throw Object.assign(new Error("--handoff is required and must summarize changed work, evidence, blockers, lease state, and next safe action."), { exitCode: 1 });
|
|
59
|
+
}
|
|
60
|
+
lease.status = "released";
|
|
61
|
+
lease.released_at = new Date().toISOString();
|
|
62
|
+
lease.release_reason = reason;
|
|
63
|
+
lease.handoff_summary = handoff.trim();
|
|
64
|
+
writeState(statePath, state);
|
|
65
|
+
return { message: `Released ${leaseId} with handoff summary.`, lease };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function selfTest() {
|
|
69
|
+
const dir = path.join(os.tmpdir(), `delano-lease-${process.pid}`);
|
|
70
|
+
const state = path.join(dir, "leases.json");
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
const acquired = acquire(state, "self-test", "delano-multi-agent-execution", "T-002", ["scripts/lease-manager.mjs"], "exclusive", 5).lease;
|
|
73
|
+
const inspected = readState(state).leases.length;
|
|
74
|
+
release(state, acquired.lease_id, "self-test complete", "validated acquire inspect release lifecycle");
|
|
75
|
+
const released = readState(state).leases[0].status;
|
|
76
|
+
rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
return { message: `Lease manager self-test passed (${inspected} acquired, ${released}).` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readState(filePath) {
|
|
81
|
+
if (!existsSync(filePath)) return { schema_version: 1, leases: [] };
|
|
82
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
83
|
+
}
|
|
84
|
+
function writeState(filePath, state) { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n"); }
|
|
85
|
+
function readOption(name) { const i = process.argv.indexOf(name); return i === -1 ? "" : process.argv[i + 1]; }
|
|
86
|
+
function readRequired(name) { const v = readOption(name); if (!v) throw Object.assign(new Error(`${name} is required.`), { exitCode: 1 }); return v; }
|
|
87
|
+
function readList(name) { const out=[]; process.argv.forEach((arg,i)=>{ if(arg===name && process.argv[i+1]) out.push(process.argv[i+1]); }); return out; }
|
|
88
|
+
function resolveRepoRoot(startDir) { for (const c of [path.resolve(startDir,".."),path.resolve(startDir,"..","..")]) if (existsSync(path.join(c,".agents"))) return c; return path.resolve(startDir,".."); }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { redactObject } = require('../common/log-safety');
|
|
4
5
|
|
|
5
6
|
const args = process.argv.slice(2);
|
|
6
7
|
if (args.length < 2) {
|
|
@@ -24,6 +25,8 @@ for (let i = 0; i < rest.length; i++) {
|
|
|
24
25
|
event.meta[key] = value;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
event.meta = redactObject(event.meta);
|
|
29
|
+
|
|
27
30
|
const root = process.cwd();
|
|
28
31
|
const logDir = path.join(root, '.agents', 'logs');
|
|
29
32
|
const logFile = path.join(logDir, 'changes.jsonl');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { buildDriftReport } from "./build-drift-report.mjs";
|
|
5
|
+
import { readLocalSyncMap } from "./read-local-sync-map.mjs";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const repoRoot = resolveRepoRoot(__dirname);
|
|
10
|
+
const args = new Set(process.argv.slice(2));
|
|
11
|
+
const jsonMode = args.has("--json");
|
|
12
|
+
const apply = args.has("--apply");
|
|
13
|
+
const approve = readOption("--approve");
|
|
14
|
+
|
|
15
|
+
const report = buildDriftReport(readLocalSyncMap(repoRoot), {}, {}, { localOnlyMode: true });
|
|
16
|
+
const plan = buildRepairPlan(report, { apply, approve });
|
|
17
|
+
|
|
18
|
+
if (jsonMode) console.log(JSON.stringify(plan, null, 2));
|
|
19
|
+
else {
|
|
20
|
+
console.log(`Repair plan produced ${plan.summary.action_count} action(s).`);
|
|
21
|
+
console.log(`Apply gate: ${plan.apply_gate.status}.`);
|
|
22
|
+
for (const action of plan.actions) console.log(`- ${action.mode}: ${action.target} ${action.summary}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (apply && plan.apply_gate.status !== "approved") {
|
|
26
|
+
if (!jsonMode) console.error(`Refusing apply: ${plan.apply_gate.reason}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildRepairPlan(report, options = {}) {
|
|
31
|
+
const actions = (report.repair_recommendations || []).map((recommendation) => ({
|
|
32
|
+
id: recommendation.id,
|
|
33
|
+
mode: "dry-run-plan",
|
|
34
|
+
drift_type: recommendation.drift_type,
|
|
35
|
+
target: recommendation.target,
|
|
36
|
+
summary: recommendation.summary,
|
|
37
|
+
proposed_action: recommendation.proposed_action,
|
|
38
|
+
apply_posture: recommendation.apply_posture || "dry-run-plan-first",
|
|
39
|
+
evidence: recommendation.evidence || {}
|
|
40
|
+
}));
|
|
41
|
+
const token = `APPLY-${report.schema_version}-${report.summary.drift_count}-${report.summary.repair_count}`;
|
|
42
|
+
const approved = options.apply && options.approve === token;
|
|
43
|
+
return {
|
|
44
|
+
schema_version: 1,
|
|
45
|
+
mode: options.apply ? "apply-request" : "dry-run-plan",
|
|
46
|
+
source_report: { schema_version: report.schema_version, generated_at: report.generated_at, drift_count: report.summary.drift_count },
|
|
47
|
+
apply_gate: {
|
|
48
|
+
status: approved ? "approved" : "blocked",
|
|
49
|
+
required_token: token,
|
|
50
|
+
provided_token: options.approve || "",
|
|
51
|
+
reason: approved ? "Explicit operator token matched; caller may perform a separate apply step." : "Explicit operator approval token is required before any local or remote mutation."
|
|
52
|
+
},
|
|
53
|
+
summary: { action_count: actions.length, dry_run: !approved, mutation_count: 0 },
|
|
54
|
+
actions
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readOption(name) {
|
|
59
|
+
const index = process.argv.indexOf(name);
|
|
60
|
+
return index === -1 ? "" : process.argv[index + 1];
|
|
61
|
+
}
|
|
62
|
+
function resolveRepoRoot(startDir) {
|
|
63
|
+
const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
|
|
64
|
+
for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
|
|
65
|
+
return path.resolve(startDir, "..");
|
|
66
|
+
}
|