@bvdm/delano 0.1.5 → 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.
Files changed (145) hide show
  1. package/.delano/README.md +7 -0
  2. package/.delano/viewer/README.md +19 -0
  3. package/.delano/viewer/public/app.js +818 -0
  4. package/.delano/viewer/public/explorer.svg +3 -0
  5. package/.delano/viewer/public/index.html +21 -0
  6. package/.delano/viewer/public/markdown.svg +6 -0
  7. package/.delano/viewer/public/styles.css +1042 -0
  8. package/.delano/viewer/public/vscode.svg +24 -0
  9. package/.delano/viewer/server.js +389 -0
  10. package/HANDBOOK.md +66 -45
  11. package/README.md +21 -2
  12. package/assets/install-manifest.json +112 -23
  13. package/assets/payload/.agents/README.md +31 -6
  14. package/assets/payload/.agents/adapters/claude/README.md +22 -3
  15. package/assets/payload/.agents/adapters/codex/README.md +22 -3
  16. package/assets/payload/.agents/adapters/opencode/README.md +22 -3
  17. package/assets/payload/.agents/adapters/pi/README.md +22 -3
  18. package/assets/payload/.agents/common/log-safety.js +55 -0
  19. package/assets/payload/.agents/eval-fixtures/skill-output/invalid/missing-evidence/output.json +6 -0
  20. package/assets/payload/.agents/eval-fixtures/skill-output/valid/summary/output.json +7 -0
  21. package/assets/payload/.agents/fixtures/github/status-snapshot.json +6 -0
  22. package/assets/payload/.agents/fixtures/linear/issue-snapshot.json +6 -0
  23. package/assets/payload/.agents/hooks/bash-worktree-fix.sh +2 -1
  24. package/assets/payload/.agents/hooks/post-tool-logger.js +2 -1
  25. package/assets/payload/.agents/hooks/session-tracker.js +0 -0
  26. package/assets/payload/.agents/hooks/user-prompt-logger.js +17 -1
  27. package/assets/payload/.agents/logs/delivery-metrics.md +22 -0
  28. package/assets/payload/.agents/logs/schema.md +20 -1
  29. package/assets/payload/.agents/rules/delivery-modes.md +17 -0
  30. package/assets/payload/.agents/schemas/README.md +22 -0
  31. package/assets/payload/.agents/schemas/artifact-scope.json +237 -0
  32. package/assets/payload/.agents/schemas/artifacts/context.schema.json +11 -0
  33. package/assets/payload/.agents/schemas/artifacts/decision_log.schema.json +12 -0
  34. package/assets/payload/.agents/schemas/artifacts/evidence.schema.json +17 -0
  35. package/assets/payload/.agents/schemas/artifacts/plan.schema.json +83 -0
  36. package/assets/payload/.agents/schemas/artifacts/spec.schema.json +101 -0
  37. package/assets/payload/.agents/schemas/artifacts/task.schema.json +121 -0
  38. package/assets/payload/.agents/schemas/artifacts/update.schema.json +12 -0
  39. package/assets/payload/.agents/schemas/artifacts/workstream.schema.json +66 -0
  40. package/assets/payload/.agents/schemas/evidence-map.json +53 -0
  41. package/assets/payload/.agents/schemas/learning/closeout-learning-proposal.schema.json +20 -0
  42. package/assets/payload/.agents/schemas/learning/delivery-metric-event.schema.json +21 -0
  43. package/assets/payload/.agents/schemas/leases/lease.schema.json +39 -0
  44. package/assets/payload/.agents/schemas/metrics/delivery-event.schema.json +29 -0
  45. package/assets/payload/.agents/schemas/metrics/delivery-events.schema.json +49 -0
  46. package/assets/payload/.agents/schemas/operating-modes.json +42 -0
  47. package/assets/payload/.agents/schemas/status-transitions.json +31 -0
  48. package/assets/payload/.agents/schemas/sync/drift-report.schema.json +25 -0
  49. package/assets/payload/.agents/schemas/sync/drift-taxonomy.json +38 -0
  50. package/assets/payload/.agents/schemas/sync/sync-map.schema.json +39 -0
  51. package/assets/payload/.agents/scripts/README.md +1 -0
  52. package/assets/payload/.agents/scripts/audit-context-files.mjs +54 -0
  53. package/assets/payload/.agents/scripts/audit-context-scoring.mjs +14 -0
  54. package/assets/payload/.agents/scripts/build-drift-report.mjs +133 -0
  55. package/assets/payload/.agents/scripts/check-artifact-schemas.mjs +116 -0
  56. package/assets/payload/.agents/scripts/check-closeout-learning-proposals.mjs +23 -0
  57. package/assets/payload/.agents/scripts/check-context-audit.mjs +61 -0
  58. package/assets/payload/.agents/scripts/check-delivery-metric-events.mjs +35 -0
  59. package/assets/payload/.agents/scripts/check-delivery-metrics.mjs +52 -0
  60. package/assets/payload/.agents/scripts/check-evidence-map.mjs +143 -0
  61. package/assets/payload/.agents/scripts/check-github-status-inspection.mjs +93 -0
  62. package/assets/payload/.agents/scripts/check-github-sync.mjs +159 -0
  63. package/assets/payload/.agents/scripts/check-handoff-summaries.mjs +57 -0
  64. package/assets/payload/.agents/scripts/check-lease-conflicts.mjs +24 -0
  65. package/assets/payload/.agents/scripts/check-lease-contracts.mjs +17 -0
  66. package/assets/payload/.agents/scripts/check-linear-issue-inspection.mjs +63 -0
  67. package/assets/payload/.agents/scripts/check-local-sync-map.mjs +151 -0
  68. package/assets/payload/.agents/scripts/check-log-safety.sh +62 -0
  69. package/assets/payload/.agents/scripts/check-operating-modes.mjs +99 -0
  70. package/assets/payload/.agents/scripts/check-path-standards.sh +1 -1
  71. package/assets/payload/.agents/scripts/check-skill-output-evals.mjs +13 -0
  72. package/assets/payload/.agents/scripts/check-status-transitions.mjs +169 -0
  73. package/assets/payload/.agents/scripts/check-strict-fixtures.mjs +140 -0
  74. package/assets/payload/.agents/scripts/check-sync-schemas.mjs +52 -0
  75. package/assets/payload/.agents/scripts/check-text-safety.mjs +158 -0
  76. package/assets/payload/.agents/scripts/check-worktree-health.mjs +100 -0
  77. package/assets/payload/.agents/scripts/fix-path-standards.sh +1 -1
  78. package/assets/payload/.agents/scripts/git-sparse-download.sh +0 -0
  79. package/assets/payload/.agents/scripts/inspect-github-sync.mjs +108 -0
  80. package/assets/payload/.agents/scripts/lease-manager.mjs +88 -0
  81. package/assets/payload/.agents/scripts/log-event.js +3 -0
  82. package/assets/payload/.agents/scripts/log-event.sh +0 -0
  83. package/assets/payload/.agents/scripts/plan-sync-repairs.mjs +66 -0
  84. package/assets/payload/.agents/scripts/pm/blocked.sh +0 -0
  85. package/assets/payload/.agents/scripts/pm/epic-list.sh +0 -0
  86. package/assets/payload/.agents/scripts/pm/in-progress.sh +0 -0
  87. package/assets/payload/.agents/scripts/pm/init.sh +0 -0
  88. package/assets/payload/.agents/scripts/pm/next.sh +0 -0
  89. package/assets/payload/.agents/scripts/pm/prd-list.sh +0 -0
  90. package/assets/payload/.agents/scripts/pm/search.sh +0 -0
  91. package/assets/payload/.agents/scripts/pm/standup.sh +0 -0
  92. package/assets/payload/.agents/scripts/pm/status.sh +0 -0
  93. package/assets/payload/.agents/scripts/pm/validate.sh +657 -2
  94. package/assets/payload/.agents/scripts/propose-closeout-learning.mjs +20 -0
  95. package/assets/payload/.agents/scripts/query-log.sh +0 -0
  96. package/assets/payload/.agents/scripts/read-local-sync-map.mjs +135 -0
  97. package/assets/payload/.agents/scripts/select-next-task.mjs +22 -0
  98. package/assets/payload/.agents/scripts/summarize-project-metrics.mjs +15 -0
  99. package/assets/payload/.agents/scripts/test-and-log.sh +0 -0
  100. package/assets/payload/.agents/skills/README.md +6 -0
  101. package/assets/payload/.agents/skills/closeout-skill/SKILL.md +3 -0
  102. package/assets/payload/.agents/skills/closeout-skill/references/runbook.md +5 -2
  103. package/assets/payload/.agents/skills/closeout-skill/templates/closure-checklist.md +2 -0
  104. package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposal.md +21 -0
  105. package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposals.md +25 -0
  106. package/assets/payload/.agents/skills/manage-context/SKILL.md +55 -0
  107. package/assets/payload/.agents/skills/manage-context/references/context-audit-checklist.md +26 -0
  108. package/assets/payload/.agents/skills/manage-context/references/runbook.md +26 -0
  109. package/assets/payload/.agents/skills/manage-context/templates/context-debt-report.md +22 -0
  110. package/assets/payload/.agents/skills/manage-context/templates/context-refresh-summary.md +13 -0
  111. package/assets/payload/.agents/skills/onboarding/SKILL.md +49 -0
  112. package/assets/payload/.agents/skills/onboarding/references/agents-md-best-practices.md +76 -0
  113. package/assets/payload/.agents/skills/prototype-skill/SKILL.md +51 -0
  114. package/assets/payload/.agents/skills/prototype-skill/references/probe-design-checklist.md +26 -0
  115. package/assets/payload/.agents/skills/prototype-skill/references/runbook.md +27 -0
  116. package/assets/payload/.agents/skills/prototype-skill/templates/probe-approval-recommendation.md +13 -0
  117. package/assets/payload/.agents/skills/prototype-skill/templates/probe-findings.md +16 -0
  118. package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/dependency.md +18 -0
  119. package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/task.md +24 -0
  120. package/assets/payload/.agents/validation-fixtures/strict/invalid/invalid-transition/task.md +20 -0
  121. package/assets/payload/.agents/validation-fixtures/strict/invalid/missing-evidence/task.md +27 -0
  122. package/assets/payload/.agents/validation-fixtures/strict/invalid/path-leak/task.md +27 -0
  123. package/assets/payload/.agents/validation-fixtures/strict/invalid/stale-context/context.md +9 -0
  124. package/assets/payload/.agents/validation-fixtures/strict/manifest.json +11 -0
  125. package/assets/payload/.agents/validation-fixtures/strict/valid/minimal-project/task.md +27 -0
  126. package/assets/payload/.delano/viewer/README.md +19 -0
  127. package/assets/payload/.delano/viewer/public/app.js +818 -0
  128. package/assets/payload/.delano/viewer/public/explorer.svg +3 -0
  129. package/assets/payload/.delano/viewer/public/index.html +21 -0
  130. package/assets/payload/.delano/viewer/public/markdown.svg +6 -0
  131. package/assets/payload/.delano/viewer/public/styles.css +1042 -0
  132. package/assets/payload/.delano/viewer/public/vscode.svg +24 -0
  133. package/assets/payload/.delano/viewer/server.js +389 -0
  134. package/assets/payload/.project/templates/plan.md +1 -1
  135. package/assets/payload/.project/templates/spec.md +1 -1
  136. package/assets/payload/.project/templates/task.md +1 -0
  137. package/assets/payload/HANDBOOK.md +66 -45
  138. package/assets/payload/install-delano.sh +0 -0
  139. package/install-delano.sh +0 -0
  140. package/package.json +31 -2
  141. package/src/cli/commands/onboarding.js +29 -0
  142. package/src/cli/commands/viewer.js +81 -0
  143. package/src/cli/index.js +20 -0
  144. package/src/cli/lib/install.js +1 -0
  145. package/src/cli/lib/onboarding.js +243 -0
@@ -0,0 +1,143 @@
1
+ import { existsSync, readdirSync, 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 contractPath = path.join(repoRoot, ".agents", "schemas", "evidence-map.json");
9
+ const projectsRoot = path.join(repoRoot, ".project", "projects");
10
+ const errors = [];
11
+ const warnings = [];
12
+
13
+ const contract = readJson(contractPath, "evidence map contract");
14
+ const strictSince = Date.parse(contract.strict_since || "2026-04-29T00:00:00Z");
15
+ const proofRules = Array.isArray(contract.done_task_rules) ? contract.done_task_rules : [];
16
+ if (contract.schema_version !== 1) errors.push("evidence-map.json schema_version must be 1.");
17
+ for (const requiredRule of ["acceptance-criteria-checked", "evidence-log-present", "validation-proof"]) {
18
+ if (!proofRules.some((rule) => rule.id === requiredRule)) errors.push(`evidence map contract missing rule: ${requiredRule}`);
19
+ }
20
+
21
+ for (const taskFile of listTaskFiles(projectsRoot)) {
22
+ const text = readFileSync(taskFile, "utf8");
23
+ const frontmatter = parseFrontmatter(taskFile, text);
24
+ if (frontmatter.status !== "done") continue;
25
+
26
+ const strict = isStrictTask(frontmatter, strictSince);
27
+ const taskErrors = validateDoneTask(taskFile, text, proofRules);
28
+ if (strict) errors.push(...taskErrors);
29
+ else warnings.push(...taskErrors);
30
+ }
31
+
32
+ if (errors.length > 0) {
33
+ console.error("Evidence map check failed:");
34
+ for (const error of errors) console.error(`- ${error}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ if (warnings.length > 0) {
39
+ console.warn(`Evidence map check warnings: ${warnings.length} legacy evidence warning(s).`);
40
+ }
41
+ console.log("Evidence map check passed for done task acceptance criteria.");
42
+
43
+ function validateDoneTask(taskFile, text, proofRules) {
44
+ const localErrors = [];
45
+ const acceptanceSection = section(text, "Acceptance Criteria");
46
+ const evidenceSection = section(text, "Evidence Log");
47
+ const evidenceText = evidenceSection || "";
48
+ const fullEvidenceContext = `${evidenceText}\n${text}`;
49
+ const criteria = acceptanceSection.split("\n").filter((line) => /^- \[[ xX]\]/.test(line.trim()));
50
+
51
+ for (const criterion of criteria) {
52
+ if (!/^- \[[xX]\]/.test(criterion.trim())) {
53
+ localErrors.push(`${toRepoPath(taskFile)} is done but has unchecked acceptance criterion: ${criterion.trim()}`);
54
+ }
55
+ }
56
+
57
+ const implementationEvidence = evidenceSection
58
+ .split("\n")
59
+ .filter((line) => /^- \d{4}-\d{2}-\d{2}/.test(line.trim()) && !line.toLowerCase().includes("implementation evidence pending"));
60
+ if (implementationEvidence.length === 0) {
61
+ localErrors.push(`${toRepoPath(taskFile)} is done but lacks implementation evidence in Evidence Log.`);
62
+ }
63
+
64
+ for (const rule of proofRules) {
65
+ if (!rule.acceptance_criterion_contains) continue;
66
+ const matchingCriterion = criteria.find((criterion) => criterion.toLowerCase().includes(rule.acceptance_criterion_contains.toLowerCase()));
67
+ if (!matchingCriterion) continue;
68
+ const proofTerms = Array.isArray(rule.proof_terms) ? rule.proof_terms : [];
69
+ if (proofTerms.length === 0) continue;
70
+ const hasProof = proofTerms.some((term) => fullEvidenceContext.toLowerCase().includes(term.toLowerCase()));
71
+ if (!hasProof) {
72
+ localErrors.push(`${toRepoPath(taskFile)} criterion lacks mapped evidence proof for rule ${rule.id}: ${matchingCriterion.trim()}`);
73
+ }
74
+ }
75
+
76
+ return localErrors;
77
+ }
78
+
79
+ function isStrictTask(frontmatter, strictSince) {
80
+ const updated = Date.parse(frontmatter.updated || "");
81
+ if (!Number.isNaN(updated) && updated >= strictSince) return true;
82
+ const created = Date.parse(frontmatter.created || "");
83
+ return !Number.isNaN(created) && created >= strictSince;
84
+ }
85
+
86
+ function section(text, heading) {
87
+ const lines = text.split("\n");
88
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
89
+ if (start === -1) return "";
90
+ const collected = [];
91
+ for (const line of lines.slice(start + 1)) {
92
+ if (line.startsWith("## ")) break;
93
+ collected.push(line);
94
+ }
95
+ return collected.join("\n").trim();
96
+ }
97
+
98
+ function parseFrontmatter(filePath, text) {
99
+ const match = text.match(/^---\n([\s\S]*?)\n---\n/);
100
+ if (!match) { errors.push(`${toRepoPath(filePath)} is missing frontmatter.`); return {}; }
101
+ const result = {};
102
+ for (const line of match[1].split("\n")) {
103
+ const index = line.indexOf(":");
104
+ if (index === -1) continue;
105
+ result[line.slice(0, index).trim()] = line.slice(index + 1).trim();
106
+ }
107
+ return result;
108
+ }
109
+
110
+ function listTaskFiles(root) {
111
+ if (!existsSync(root)) return [];
112
+ const files = [];
113
+ for (const project of readdirSync(root, { withFileTypes: true })) {
114
+ if (!project.isDirectory()) continue;
115
+ const tasksDir = path.join(root, project.name, "tasks");
116
+ if (!existsSync(tasksDir)) continue;
117
+ for (const task of readdirSync(tasksDir, { withFileTypes: true })) {
118
+ if (task.isFile() && task.name.endsWith(".md")) files.push(path.join(tasksDir, task.name));
119
+ }
120
+ }
121
+ return files;
122
+ }
123
+
124
+ function readJson(filePath, label) {
125
+ try { return JSON.parse(readFileSync(filePath, "utf8")); }
126
+ catch (error) { errors.push(`Could not read ${label} at ${toRepoPath(filePath)}: ${error.message}`); return {}; }
127
+ }
128
+
129
+ function resolveRepoRoot(startDir) {
130
+ const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
131
+ for (const candidate of candidates) {
132
+ if (existsSync(path.join(candidate, ".project", "projects")) && existsSync(path.join(candidate, ".agents"))) return candidate;
133
+ }
134
+ return path.resolve(startDir, "..");
135
+ }
136
+
137
+ function escapeRegExp(value) {
138
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139
+ }
140
+
141
+ function toRepoPath(filePath) {
142
+ return path.relative(repoRoot, filePath).split(path.sep).join("/");
143
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const repoRoot = resolveRepoRoot(__dirname);
9
+ const errors = [];
10
+ const warnings = [];
11
+ const snapshotPath = path.join(repoRoot, ".agents", "fixtures", "github", "status-snapshot.json");
12
+ const syncMap = readLocalSyncMap();
13
+ const snapshot = readJson(snapshotPath, "GitHub status snapshot", { repositories: [] });
14
+ const snapshotIndex = indexSnapshot(snapshot);
15
+
16
+ if (snapshot.schema_version !== 1) errors.push("GitHub status snapshot schema_version must be 1.");
17
+
18
+ for (const project of syncMap.projects || []) {
19
+ for (const task of project.tasks || []) {
20
+ inspectRef(project, task, "github_issue", "issue");
21
+ inspectRef(project, task, "github_pr", "pull_request");
22
+ }
23
+ }
24
+
25
+ if (errors.length > 0) {
26
+ console.error("GitHub status inspection failed:");
27
+ for (const error of errors) console.error(`- ${error}`);
28
+ process.exit(1);
29
+ }
30
+ console.log(`GitHub status inspection passed with ${warnings.length} local-only warning(s).`);
31
+ if (process.argv.includes("--verbose")) for (const warning of warnings) console.warn(`Warning: ${warning}`);
32
+
33
+ function inspectRef(project, task, field, kind) {
34
+ const value = task[field];
35
+ if (!value) return;
36
+ const parsed = parseGitHubRef(value, project.github_repo);
37
+ if (!parsed) {
38
+ errors.push(`${project.slug}/${task.local_id} has invalid ${field}: ${value}`);
39
+ return;
40
+ }
41
+ if (parsed.kind !== kind) errors.push(`${project.slug}/${task.local_id} expected ${kind} but got ${parsed.kind}: ${value}`);
42
+ const key = `${parsed.owner}/${parsed.repo}#${parsed.number}:${parsed.kind}`;
43
+ const remote = snapshotIndex.get(key);
44
+ if (!remote) {
45
+ warnings.push(`${project.slug}/${task.local_id} ${field} ${key} has no mock snapshot; treated as local-only.`);
46
+ return;
47
+ }
48
+ if (!remote.state) errors.push(`${project.slug}/${task.local_id} ${field} ${key} snapshot lacks state.`);
49
+ if (kind === "pull_request" && remote.mergeable === "unknown") warnings.push(`${project.slug}/${task.local_id} PR ${key} has unknown mergeability.`);
50
+ }
51
+
52
+ function parseGitHubRef(value, fallbackRepo) {
53
+ const raw = String(value || "").trim();
54
+ if (!raw) return null;
55
+ const url = raw.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/([0-9]+)$/);
56
+ if (url) return { owner: url[1], repo: url[2], kind: url[3] === "pull" ? "pull_request" : "issue", number: Number(url[4]) };
57
+ const short = raw.match(/^#?([0-9]+)$/);
58
+ if (short && fallbackRepo && fallbackRepo.includes("/")) {
59
+ const [owner, repo] = fallbackRepo.split("/");
60
+ return { owner, repo, kind: "issue", number: Number(short[1]) };
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function indexSnapshot(snapshot) {
66
+ const index = new Map();
67
+ for (const repo of snapshot.repositories || []) {
68
+ const owner = repo.owner;
69
+ const name = repo.name;
70
+ for (const issue of repo.issues || []) index.set(`${owner}/${name}#${issue.number}:issue`, issue);
71
+ for (const pr of repo.pull_requests || []) index.set(`${owner}/${name}#${pr.number}:pull_request`, pr);
72
+ }
73
+ return index;
74
+ }
75
+
76
+ function readLocalSyncMap() {
77
+ const result = spawnSync(process.execPath, [path.join(repoRoot, "scripts", "check-local-sync-map.mjs"), "--json"], { cwd: repoRoot, encoding: "utf8" });
78
+ if (result.status !== 0) {
79
+ errors.push(`local sync map reader failed: ${result.stderr || result.stdout}`);
80
+ return { projects: [] };
81
+ }
82
+ const parsed = JSON.parse(result.stdout);
83
+ return parsed.sync_map || parsed;
84
+ }
85
+ function readJson(filePath, label, fallback) {
86
+ try { return JSON.parse(readFileSync(filePath, "utf8")); }
87
+ catch (error) { errors.push(`Could not read ${label}: ${error.message}`); return fallback; }
88
+ }
89
+ function resolveRepoRoot(startDir) {
90
+ const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
91
+ for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
92
+ return path.resolve(startDir, "..");
93
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
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 allowNetwork = args.has("--fetch");
13
+ const syncMapPath = readOption("--sync-map");
14
+ const statePath = readOption("--github-state") || readOption("--fixture");
15
+
16
+ const syncMap = syncMapPath ? readJson(syncMapPath) : readLocalSyncMap(repoRoot);
17
+ const githubState = statePath ? readJson(statePath) : (allowNetwork ? fetchGithubState(syncMap) : { repositories: {}, source: "none" });
18
+ const report = inspectGithubSync(syncMap, githubState, { fetched: allowNetwork && !statePath });
19
+
20
+ if (jsonMode) {
21
+ console.log(JSON.stringify(report, null, 2));
22
+ } else {
23
+ console.log(`GitHub sync inspection checked ${report.summary.checked_refs} refs; ${report.summary.drift_count} drift item(s).`);
24
+ if (!allowNetwork && !statePath) console.log("No GitHub state supplied; use --github-state <file> for deterministic dry-run inspection or --fetch to query gh.");
25
+ for (const drift of report.drift) console.log(`- ${drift.severity}: ${drift.task} ${drift.summary}`);
26
+ }
27
+
28
+ if (report.errors.length > 0) {
29
+ if (!jsonMode) {
30
+ console.error("GitHub sync inspection failed:");
31
+ for (const error of report.errors) console.error(`- ${error}`);
32
+ }
33
+ process.exit(1);
34
+ }
35
+
36
+ export function inspectGithubSync(syncMap, githubState = {}, options = {}) {
37
+ const errors = [];
38
+ const inspections = [];
39
+ const drift = [];
40
+ for (const project of syncMap.projects || []) {
41
+ const repo = project.github_repo || githubState.default_repository || "";
42
+ for (const task of project.tasks || []) {
43
+ for (const field of ["github_issue", "github_pr"]) {
44
+ const localRef = task[field];
45
+ if (!localRef) continue;
46
+ const type = field === "github_pr" ? "pull_request" : "issue";
47
+ const parsed = parseGithubRef(localRef, repo, type);
48
+ const taskKey = `${project.slug}:${task.local_id}`;
49
+ if (!parsed) {
50
+ errors.push(`${taskKey} has unsupported ${field}: ${localRef}`);
51
+ continue;
52
+ }
53
+ const external = lookupGithubState(githubState, parsed);
54
+ const inspection = {
55
+ task: taskKey,
56
+ field,
57
+ local_ref: localRef,
58
+ repository: parsed.repository,
59
+ number: parsed.number,
60
+ type,
61
+ external_state: external?.state || "unknown",
62
+ external_url: external?.url || parsed.url || "",
63
+ source: external?.source || githubState.source || (options.fetched ? "gh" : "fixture-or-none")
64
+ };
65
+ inspections.push(inspection);
66
+ if (!external) {
67
+ drift.push({
68
+ drift_type: "orphan-drift",
69
+ severity: "warning",
70
+ task: taskKey,
71
+ target: `${parsed.repository}#${parsed.number}`,
72
+ summary: `references ${field} without inspected GitHub state`,
73
+ evidence: { local_ref: localRef }
74
+ });
75
+ } else if (external.kind && external.kind !== type) {
76
+ drift.push({
77
+ drift_type: "mapping-drift",
78
+ severity: "error",
79
+ task: taskKey,
80
+ target: `${parsed.repository}#${parsed.number}`,
81
+ summary: `local ${field} points at inspected ${external.kind}`,
82
+ evidence: { local_ref: localRef, external_kind: external.kind }
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return {
89
+ schema_version: 1,
90
+ source: githubState.source || (options.fetched ? "gh" : "local-dry-run"),
91
+ summary: { checked_refs: inspections.length, drift_count: drift.length },
92
+ inspections,
93
+ drift,
94
+ errors
95
+ };
96
+ }
97
+
98
+ function parseGithubRef(ref, projectRepo, type) {
99
+ const value = String(ref || "").trim();
100
+ if (!value) return null;
101
+ const urlMatch = value.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(issues|pull)\/(\d+)/);
102
+ if (urlMatch) return { repository: urlMatch[1], number: urlMatch[3], url: value };
103
+ const shorthand = value.match(/^([^/\s]+\/[^#\s]+)#(\d+)$/);
104
+ if (shorthand) return { repository: shorthand[1], number: shorthand[2] };
105
+ const number = value.match(/^#?(\d+)$/);
106
+ if (number && projectRepo) return { repository: projectRepo, number: number[1] };
107
+ return null;
108
+ }
109
+
110
+ function lookupGithubState(state, parsed) {
111
+ const repo = state.repositories?.[parsed.repository] || state.repos?.[parsed.repository];
112
+ if (!repo) return null;
113
+ const pr = repo.pull_requests?.[parsed.number] || repo.prs?.[parsed.number];
114
+ if (pr) return { ...pr, kind: "pull_request", source: state.source || "fixture" };
115
+ const issue = repo.issues?.[parsed.number];
116
+ if (issue) return { ...issue, kind: "issue", source: state.source || "fixture" };
117
+ return null;
118
+ }
119
+
120
+ function fetchGithubState(syncMap) {
121
+ const repositories = {};
122
+ if (!commandExists("gh")) return { repositories, source: "gh-unavailable" };
123
+ for (const project of syncMap.projects || []) {
124
+ const repo = project.github_repo;
125
+ if (!repo) continue;
126
+ repositories[repo] ||= { issues: {}, pull_requests: {} };
127
+ for (const task of project.tasks || []) {
128
+ const refs = [task.github_issue, task.github_pr].filter(Boolean);
129
+ for (const ref of refs) {
130
+ const parsed = parseGithubRef(ref, repo);
131
+ if (!parsed) continue;
132
+ const result = spawnSync("gh", ["issue", "view", parsed.number, "--repo", parsed.repository, "--json", "number,state,url"], { encoding: "utf8" });
133
+ if (result.status === 0) {
134
+ const issue = JSON.parse(result.stdout);
135
+ repositories[parsed.repository] ||= { issues: {}, pull_requests: {} };
136
+ repositories[parsed.repository].issues[String(issue.number)] = { state: issue.state, url: issue.url };
137
+ }
138
+ }
139
+ }
140
+ }
141
+ return { repositories, source: "gh" };
142
+ }
143
+
144
+ function readOption(name) {
145
+ const index = process.argv.indexOf(name);
146
+ return index === -1 ? "" : process.argv[index + 1];
147
+ }
148
+ function readJson(filePath) {
149
+ return JSON.parse(readFileSync(filePath, "utf8"));
150
+ }
151
+ function commandExists(name) {
152
+ const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [name] : ["-v", name], { shell: process.platform !== "win32", stdio: "ignore" });
153
+ return result.status === 0;
154
+ }
155
+ function resolveRepoRoot(startDir) {
156
+ const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
157
+ for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
158
+ return path.resolve(startDir, "..");
159
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const repoRoot = resolveRepoRoot(__dirname);
10
+ const statePath = readOption("--state") || path.join(repoRoot, ".agents", "leases", "active-leases.json");
11
+ const selfTest = process.argv.includes("--self-test") || !existsSync(statePath);
12
+ const errors = [];
13
+
14
+ if (selfTest) runSelfTest();
15
+ else validateState(statePath);
16
+
17
+ if (process.argv.includes("--json")) console.log(JSON.stringify({ schema_version: 1, ok: errors.length === 0, error_count: errors.length, errors }, null, 2));
18
+ else console.log(`Handoff summary check ${errors.length ? "failed" : "passed"} with ${errors.length} error(s).`);
19
+
20
+ if (errors.length) {
21
+ if (!process.argv.includes("--json")) for (const error of errors) console.error(`- ${error}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ function validateState(filePath) {
26
+ const state = JSON.parse(readFileSync(filePath, "utf8"));
27
+ for (const lease of state.leases || []) {
28
+ if (lease.status !== "released") continue;
29
+ validateSummary(lease.handoff_summary || "", `${lease.project}/${lease.task_id}`);
30
+ }
31
+ }
32
+
33
+ function runSelfTest() {
34
+ const dir = path.join(os.tmpdir(), `delano-handoff-${process.pid}`);
35
+ const state = path.join(dir, "leases.json");
36
+ mkdirSync(dir, { recursive: true });
37
+ try {
38
+ const acquire = spawnSync(process.execPath, ["scripts/lease-manager.mjs", "acquire", "--state", state, "--owner", "handoff-test", "--project", "delano-multi-agent-execution", "--task", "T-006", "--zone", "scripts/lease-manager.mjs"], { cwd: repoRoot, encoding: "utf8" });
39
+ if (acquire.status !== 0) { errors.push(`self-test acquire failed: ${acquire.stderr || acquire.stdout}`); return; }
40
+ const lease = JSON.parse(readFileSync(state, "utf8")).leases[0];
41
+ const missing = spawnSync(process.execPath, ["scripts/lease-manager.mjs", "release", "--state", state, "--lease-id", lease.lease_id], { cwd: repoRoot, encoding: "utf8" });
42
+ if (missing.status === 0) errors.push("release without --handoff should be rejected for active stream closeout");
43
+ const summary = "Changed: validated handoff requirement\nEvidence: self-test release gate\nBlockers: none\nLease state: released\nNext safe action: continue";
44
+ const release = spawnSync(process.execPath, ["scripts/lease-manager.mjs", "release", "--state", state, "--lease-id", lease.lease_id, "--handoff", summary], { cwd: repoRoot, encoding: "utf8" });
45
+ if (release.status !== 0) errors.push(`self-test release with handoff failed: ${release.stderr || release.stdout}`);
46
+ validateState(state);
47
+ } finally {
48
+ rmSync(dir, { recursive: true, force: true });
49
+ }
50
+ }
51
+
52
+ function validateSummary(summary, target) {
53
+ const required = ["Changed:", "Evidence:", "Blockers:", "Lease state:", "Next safe action:"];
54
+ for (const heading of required) if (!summary.includes(heading)) errors.push(`${target} handoff_summary missing ${heading}`);
55
+ }
56
+ function readOption(name) { const i = process.argv.indexOf(name); return i === -1 ? "" : process.argv[i + 1]; }
57
+ 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,".."); }
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ const repoRoot = resolveRepoRoot(__dirname);
7
+ const statePath = readOption("--state") || path.join(repoRoot, ".agents", "leases", "active-leases.json");
8
+ const requestedZones = readList("--zone");
9
+ const requestedMode = readOption("--mode") || "shared";
10
+ const state = readState(statePath);
11
+ const active = (state.leases || []).filter((lease) => lease.status === "active" && new Date(lease.expires_at).getTime() > Date.now());
12
+ const conflicts = [];
13
+ for (const lease of active) {
14
+ const overlap = requestedZones.length ? lease.conflict_zones.filter((zone) => requestedZones.includes(zone)) : [];
15
+ if (!requestedZones.length) continue;
16
+ if (overlap.length && (lease.mode === "exclusive" || requestedMode === "exclusive")) conflicts.push({ lease_id: lease.lease_id, owner: lease.owner, zones: overlap, mode: lease.mode });
17
+ }
18
+ if (process.argv.includes("--json")) console.log(JSON.stringify({ schema_version: 1, conflict_count: conflicts.length, conflicts }, null, 2));
19
+ else console.log(`Lease conflict check found ${conflicts.length} conflict(s).`);
20
+ if (conflicts.length) process.exit(2);
21
+ function readState(filePath) { if (!existsSync(filePath)) return { schema_version: 1, leases: [] }; return JSON.parse(readFileSync(filePath, "utf8")); }
22
+ function readOption(name) { const i = process.argv.indexOf(name); return i === -1 ? "" : process.argv[i + 1]; }
23
+ 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; }
24
+ 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,".."); }
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ const repoRoot = resolveRepoRoot(__dirname);
7
+ const schema = JSON.parse(readFileSync(path.join(repoRoot, ".agents", "schemas", "leases", "lease.schema.json"), "utf8"));
8
+ const errors = [];
9
+ for (const field of ["schema_version", "lease_id", "owner", "project", "task_id", "status", "mode", "conflict_zones", "acquired_at", "expires_at"]) {
10
+ if (!schema.required?.includes(field)) errors.push(`lease schema must require ${field}`);
11
+ }
12
+ for (const status of ["active", "released", "expired"]) if (!schema.properties?.status?.enum?.includes(status)) errors.push(`lease status missing ${status}`);
13
+ for (const mode of ["shared", "exclusive"]) if (!schema.properties?.mode?.enum?.includes(mode)) errors.push(`lease mode missing ${mode}`);
14
+ if (!schema.properties?.handoff_summary) errors.push("lease schema must reserve handoff_summary for release closeout");
15
+ if (errors.length) { console.error("Lease contract check failed:"); for (const e of errors) console.error(`- ${e}`); process.exit(1); }
16
+ console.log("Lease contract check passed for lifecycle fields and modes.");
17
+ 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,".."); }
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const repoRoot = resolveRepoRoot(__dirname);
9
+ const errors = [];
10
+ const warnings = [];
11
+ const snapshotPath = path.join(repoRoot, ".agents", "fixtures", "linear", "issue-snapshot.json");
12
+ const syncMap = readLocalSyncMap();
13
+ const snapshot = readJson(snapshotPath, "Linear issue snapshot", { issues: [] });
14
+ const issueIndex = new Map((snapshot.issues || []).map((issue) => [issue.id, issue]));
15
+
16
+ if (snapshot.schema_version !== 1) errors.push("Linear issue snapshot schema_version must be 1.");
17
+
18
+ for (const project of syncMap.projects || []) {
19
+ for (const task of project.tasks || []) {
20
+ if (!task.linear_issue_id) continue;
21
+ if (!/^[-_A-Za-z0-9]+$/.test(task.linear_issue_id)) {
22
+ errors.push(`${project.slug}/${task.local_id} has invalid linear_issue_id: ${task.linear_issue_id}`);
23
+ continue;
24
+ }
25
+ const issue = issueIndex.get(task.linear_issue_id);
26
+ if (!issue) {
27
+ warnings.push(`${project.slug}/${task.local_id} Linear issue ${task.linear_issue_id} has no mock snapshot; treated as local-only.`);
28
+ continue;
29
+ }
30
+ if (!issue.state) errors.push(`${task.linear_issue_id} snapshot lacks state.`);
31
+ if (issue.project_id && project.linear_project_id && issue.project_id !== project.linear_project_id) errors.push(`${project.slug}/${task.local_id} Linear project drift: ${issue.project_id} != ${project.linear_project_id}`);
32
+ for (const dependency of issue.depends_on || []) {
33
+ if (!task.depends_on?.includes(dependency.local_id || dependency)) warnings.push(`${project.slug}/${task.local_id} remote dependency not present locally: ${dependency.local_id || dependency}`);
34
+ }
35
+ }
36
+ }
37
+
38
+ if (errors.length > 0) {
39
+ console.error("Linear issue inspection failed:");
40
+ for (const error of errors) console.error(`- ${error}`);
41
+ process.exit(1);
42
+ }
43
+ console.log(`Linear issue inspection passed with ${warnings.length} local-only warning(s).`);
44
+ if (process.argv.includes("--verbose")) for (const warning of warnings) console.warn(`Warning: ${warning}`);
45
+
46
+ function readLocalSyncMap() {
47
+ const result = spawnSync(process.execPath, [path.join(repoRoot, "scripts", "check-local-sync-map.mjs"), "--json"], { cwd: repoRoot, encoding: "utf8" });
48
+ if (result.status !== 0) {
49
+ errors.push(`local sync map reader failed: ${result.stderr || result.stdout}`);
50
+ return { projects: [] };
51
+ }
52
+ const parsed = JSON.parse(result.stdout);
53
+ return parsed.sync_map || parsed;
54
+ }
55
+ function readJson(filePath, label, fallback) {
56
+ try { return JSON.parse(readFileSync(filePath, "utf8")); }
57
+ catch (error) { errors.push(`Could not read ${label}: ${error.message}`); return fallback; }
58
+ }
59
+ function resolveRepoRoot(startDir) {
60
+ const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
61
+ for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
62
+ return path.resolve(startDir, "..");
63
+ }