@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,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://delano.local/schemas/sync/drift-report.schema.json",
4
+ "title": "Delano dry-run drift report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "mode", "generated_at", "summary", "drift", "repair_recommendations"],
8
+ "properties": {
9
+ "schema_version": { "const": 1 },
10
+ "mode": { "const": "dry-run" },
11
+ "generated_at": { "type": "string" },
12
+ "summary": {
13
+ "type": "object",
14
+ "required": ["projects", "tasks", "drift_count", "repair_count"],
15
+ "properties": {
16
+ "projects": { "type": "integer", "minimum": 0 },
17
+ "tasks": { "type": "integer", "minimum": 0 },
18
+ "drift_count": { "type": "integer", "minimum": 0 },
19
+ "repair_count": { "type": "integer", "minimum": 0 }
20
+ }
21
+ },
22
+ "drift": { "type": "array" },
23
+ "repair_recommendations": { "type": "array" }
24
+ }
25
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "schema_version": 1,
3
+ "status": "draft",
4
+ "purpose": "Typed local-first taxonomy for Delano operational sync drift and repair recommendations.",
5
+ "drift_types": [
6
+ {
7
+ "id": "mapping-drift",
8
+ "description": "A local project/task/workstream mapping disagrees with the registry or external identifiers.",
9
+ "severity": ["info", "warning", "error"],
10
+ "repair_posture": "dry-run-plan-first"
11
+ },
12
+ {
13
+ "id": "status-drift",
14
+ "description": "Local status and inspected external status differ.",
15
+ "severity": ["warning", "error"],
16
+ "repair_posture": "dry-run-plan-first"
17
+ },
18
+ {
19
+ "id": "dependency-drift",
20
+ "description": "Local dependencies and inspected external dependency links differ.",
21
+ "severity": ["warning", "error"],
22
+ "repair_posture": "dry-run-plan-first"
23
+ },
24
+ {
25
+ "id": "orphan-drift",
26
+ "description": "A local or external record lacks its expected counterpart.",
27
+ "severity": ["info", "warning", "error"],
28
+ "repair_posture": "manual-review-before-apply"
29
+ },
30
+ {
31
+ "id": "repair-recommendation",
32
+ "description": "A proposed local or external repair action generated from drift inspection.",
33
+ "severity": ["info", "warning", "error"],
34
+ "repair_posture": "never-apply-without-explicit-approval"
35
+ }
36
+ ],
37
+ "repair_recommendation_fields": ["drift_type", "target", "summary", "proposed_action", "apply_posture", "evidence"]
38
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://delano.local/schemas/sync/sync-map.schema.json",
4
+ "title": "Delano operational sync map",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "projects"],
8
+ "properties": {
9
+ "schema_version": { "const": 1 },
10
+ "projects": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["slug", "local_path"],
16
+ "properties": {
17
+ "slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
18
+ "local_path": { "type": "string", "pattern": "^\\.project/projects/[^/]+$" },
19
+ "linear_project_id": { "type": "string" },
20
+ "github_repo": { "type": "string" },
21
+ "tasks": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "required": ["local_id"],
27
+ "properties": {
28
+ "local_id": { "type": "string", "pattern": "^T-[0-9]{3}$" },
29
+ "linear_issue_id": { "type": "string" },
30
+ "github_issue": { "type": "string" },
31
+ "github_pr": { "type": "string" }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
@@ -27,5 +27,6 @@ Operational:
27
27
  - `query-log.sh`
28
28
  - `test-and-log.sh`
29
29
  - `check-path-standards.sh`
30
+ - `check-text-safety.mjs`
30
31
  - `fix-path-standards.sh`
31
32
  - `git-sparse-download.sh`
@@ -0,0 +1,54 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } 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 contextDir = path.join(repoRoot, ".project", "context");
9
+ const staleDays = Number(readOption("--stale-days") || 180);
10
+ const now = Date.now();
11
+ const requiredCommandDocs = new Map([
12
+ ["tech-context.md", ["npm test", "validate.sh"]],
13
+ ["progress.md", ["validate.sh"]]
14
+ ]);
15
+
16
+ const files = existsSync(contextDir) ? readdirSync(contextDir).filter((file) => file.endsWith(".md")).sort() : [];
17
+ const entries = files.map((file) => auditFile(file));
18
+ const summary = entries.reduce((acc, entry) => {
19
+ acc[entry.classification] = (acc[entry.classification] || 0) + 1;
20
+ return acc;
21
+ }, {});
22
+ const result = {
23
+ schema_version: 1,
24
+ context_dir: ".project/context",
25
+ stale_days: staleDays,
26
+ file_count: entries.length,
27
+ summary,
28
+ files: entries
29
+ };
30
+
31
+ if (process.argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
32
+ else console.log(`Context audit scored ${entries.length} file(s): ${Object.entries(summary).map(([k,v])=>`${k}=${v}`).join(", ")}.`);
33
+
34
+ function auditFile(file) {
35
+ const repoPath = [".project", "context", file].join("/");
36
+ const abs = path.join(repoRoot, ".project", "context", file);
37
+ const text = readFileSync(abs, "utf8");
38
+ const requiredCommands = requiredCommandDocs.get(file) || [];
39
+ const missingCommands = requiredCommands.filter((command) => !text.includes(command));
40
+ const ageDays = Math.floor((now - statSync(abs).mtimeMs) / 86_400_000);
41
+ const lineCount = text.split(/\r?\n/).length;
42
+ const wordCount = (text.match(/\b\w+\b/g) || []).length;
43
+ const placeholderSignals = countSignals(text, [/\bTODO\b/i, /\bTBD\b/i, /placeholder/i, /fill this/i, /coming soon/i]);
44
+ let classification = "real";
45
+ if (file === "README.md") classification = "not_applicable";
46
+ else if (wordCount < 40 || placeholderSignals >= 2) classification = "placeholder";
47
+ else if (missingCommands.length) classification = "missing_required_commands";
48
+ else if (ageDays > staleDays) classification = "stale";
49
+ const score = classification === "real" ? 100 : classification === "stale" ? 65 : classification === "missing_required_commands" ? 55 : classification === "placeholder" ? 25 : 0;
50
+ return { path: repoPath, classification, score, age_days: ageDays, line_count: lineCount, word_count: wordCount, missing_required_commands: missingCommands };
51
+ }
52
+ function countSignals(text, patterns) { return patterns.reduce((sum, pattern) => sum + (pattern.test(text) ? 1 : 0), 0); }
53
+ function readOption(name) { const i = process.argv.indexOf(name); return i === -1 ? "" : process.argv[i + 1]; }
54
+ function resolveRepoRoot(startDir) { for (const c of [path.resolve(startDir,".."),path.resolve(startDir,"..","..")]) if (existsSync(path.join(c,".project"))) return c; return path.resolve(startDir,".."); }
@@ -0,0 +1,14 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename);
5
+ const repoRoot = resolveRepoRoot(__dirname);
6
+ const targets = ["AGENTS.md", ".project/context", ".project/registry/linear-map.json", ".agents/scripts/pm/validate.sh"];
7
+ const findings = targets.map(scoreTarget);
8
+ const summary = { real:0, placeholder:0, stale:0, missing:0, not_applicable:0 };
9
+ for (const f of findings) summary[f.classification]++;
10
+ const result = { schema_version: 1, score: findings.filter(f=>f.classification === "real").length, max_score: findings.length, summary, findings };
11
+ if (process.argv.includes("--json")) console.log(JSON.stringify(result,null,2)); else console.log(`Context audit score ${result.score}/${result.max_score}; missing=${summary.missing}; placeholder=${summary.placeholder}.`);
12
+ if (summary.missing || summary.placeholder) process.exit(1);
13
+ function scoreTarget(rel){ const abs=path.join(repoRoot, rel); if(!existsSync(abs)) return { path: rel, classification: "missing", reason: "required context path is absent" }; const st=statSync(abs); if(st.isDirectory()) return { path: rel, classification: "real", reason: "directory present" }; const text=readFileSync(abs,"utf8"); if(/TODO|placeholder|coming soon/i.test(text) && text.length < 200) return { path: rel, classification: "placeholder", reason: "short placeholder text" }; if(rel.endsWith('validate.sh') && !text.includes('Summary')) return { path: rel, classification: "stale", reason: "validation script lacks summary section" }; return { path: rel, classification: "real", reason: "required context content present" }; }
14
+ function resolveRepoRoot(startDir){ for(const c of [path.resolve(startDir,".."),path.resolve(startDir,"..","..")]) if(existsSync(path.join(c,".project"))) return c; return path.resolve(startDir,".."); }
@@ -0,0 +1,133 @@
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 localOnlyMode = process.argv.includes("--local-only") || (!process.argv.includes("--github-snapshot") && !process.argv.includes("--linear-snapshot"));
11
+ const githubSnapshotPath = readOption("--github-snapshot") || path.join(repoRoot, ".agents", "fixtures", "github", "status-snapshot.json");
12
+ const linearSnapshotPath = readOption("--linear-snapshot") || path.join(repoRoot, ".agents", "fixtures", "linear", "issue-snapshot.json");
13
+
14
+ if (isDirectRun()) {
15
+ const syncMap = readLocalSyncMap(repoRoot);
16
+ const githubSnapshot = localOnlyMode ? { repositories: [] } : readJson(githubSnapshotPath, { repositories: [] });
17
+ const linearSnapshot = localOnlyMode ? { issues: [] } : readJson(linearSnapshotPath, { issues: [] });
18
+ const report = buildDriftReport(syncMap, githubSnapshot, linearSnapshot, { localOnlyMode });
19
+
20
+ if (jsonMode) {
21
+ console.log(JSON.stringify(report, null, 2));
22
+ } else {
23
+ console.log(`Dry-run drift report produced ${report.summary.drift_count} drift item(s) from ${report.summary.tasks} task(s).`);
24
+ console.log(`Mode: ${report.mode}; apply posture: ${report.apply_posture}.`);
25
+ for (const drift of report.drift) console.log(`- ${drift.severity}: ${drift.target} ${drift.summary}`);
26
+ }
27
+ }
28
+ export function buildDriftReport(syncMap, githubSnapshot = {}, linearSnapshot = {}, options = {}) {
29
+ const githubIndex = indexGithubSnapshot(githubSnapshot);
30
+ const linearIndex = new Map((linearSnapshot.issues || []).map((issue) => [String(issue.id), issue]));
31
+ const drift = [];
32
+ let taskCount = 0;
33
+ let inspectedRefs = 0;
34
+
35
+ for (const project of syncMap.projects || []) {
36
+ for (const task of project.tasks || []) {
37
+ taskCount += 1;
38
+ const taskKey = `${project.slug}/${task.local_id}`;
39
+
40
+ for (const [field, expectedKind] of [["github_issue", "issue"], ["github_pr", "pull_request"]]) {
41
+ if (!task[field]) continue;
42
+ inspectedRefs += 1;
43
+ const parsed = parseGitHubRef(task[field], project.github_repo, expectedKind);
44
+ if (!parsed) {
45
+ drift.push(driftItem("mapping-drift", "error", taskKey, `invalid ${field}: ${task[field]}`, "Correct or remove the malformed GitHub reference.", { field, value: task[field] }));
46
+ continue;
47
+ }
48
+ const remote = githubIndex.get(`${parsed.owner}/${parsed.repo}#${parsed.number}:${parsed.kind}`);
49
+ if (!remote) {
50
+ drift.push(driftItem("orphan-drift", "warning", taskKey, `${field} has no inspected GitHub counterpart`, "Review the reference or refresh the GitHub snapshot before applying changes.", { field, ref: task[field], expected_kind: expectedKind }));
51
+ } else if (remote.kind && remote.kind !== expectedKind) {
52
+ drift.push(driftItem("mapping-drift", "error", taskKey, `${field} points to ${remote.kind}, expected ${expectedKind}`, "Move the reference to the correct local field or repair the remote mapping.", { field, ref: task[field], external_kind: remote.kind }));
53
+ }
54
+ }
55
+
56
+ if (task.linear_issue_id) {
57
+ inspectedRefs += 1;
58
+ const issue = linearIndex.get(String(task.linear_issue_id));
59
+ if (!issue) {
60
+ drift.push(driftItem("orphan-drift", "warning", taskKey, "linear_issue_id has no inspected Linear counterpart", "Review the reference or refresh the Linear snapshot before applying changes.", { linear_issue_id: task.linear_issue_id }));
61
+ } else if (issue.project_id && project.linear_project_id && issue.project_id !== project.linear_project_id) {
62
+ drift.push(driftItem("mapping-drift", "error", taskKey, `Linear project mismatch: ${issue.project_id} != ${project.linear_project_id}`, "Plan a project mapping repair before sync apply.", { linear_issue_id: task.linear_issue_id, external_project_id: issue.project_id, local_project_id: project.linear_project_id }));
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ const repairRecommendations = drift.map((item, index) => ({
69
+ id: `RR-${String(index + 1).padStart(3, "0")}`,
70
+ drift_type: item.drift_type,
71
+ target: item.target,
72
+ summary: item.summary,
73
+ proposed_action: item.proposed_action,
74
+ apply_posture: item.apply_posture,
75
+ evidence: item.evidence
76
+ }));
77
+
78
+ return {
79
+ schema_version: 1,
80
+ mode: "dry-run",
81
+ generated_at: new Date().toISOString(),
82
+ source: options.localOnlyMode ? "local-sync-map-only" : "local-sync-map-and-fixtures",
83
+ apply_posture: "never-apply-without-explicit-approval",
84
+ summary: {
85
+ projects: (syncMap.projects || []).length,
86
+ tasks: taskCount,
87
+ inspected_refs: inspectedRefs,
88
+ drift_count: drift.length,
89
+ repair_count: repairRecommendations.length
90
+ },
91
+ drift,
92
+ repair_recommendations: repairRecommendations
93
+ };
94
+ }
95
+
96
+ function driftItem(driftType, severity, target, summary, proposedAction, evidence) {
97
+ return { drift_type: driftType, severity, target, summary, proposed_action: proposedAction, apply_posture: "dry-run-plan-first", evidence };
98
+ }
99
+ function parseGitHubRef(value, fallbackRepo, expectedKind) {
100
+ const raw = String(value || "").trim();
101
+ const url = raw.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/([0-9]+)\/?$/);
102
+ if (url) return { owner: url[1], repo: url[2], kind: url[3] === "pull" ? "pull_request" : "issue", number: Number(url[4]) };
103
+ const short = raw.match(/^#?([0-9]+)$/);
104
+ if (short && fallbackRepo?.includes("/")) {
105
+ const [owner, repo] = fallbackRepo.split("/");
106
+ return { owner, repo, kind: expectedKind, number: Number(short[1]) };
107
+ }
108
+ return null;
109
+ }
110
+ function indexGithubSnapshot(snapshot) {
111
+ const index = new Map();
112
+ for (const repo of snapshot.repositories || []) {
113
+ for (const issue of repo.issues || []) index.set(`${repo.owner}/${repo.name}#${issue.number}:issue`, { ...issue, kind: "issue" });
114
+ for (const pr of repo.pull_requests || []) index.set(`${repo.owner}/${repo.name}#${pr.number}:pull_request`, { ...pr, kind: "pull_request" });
115
+ }
116
+ return index;
117
+ }
118
+ function readOption(name) {
119
+ const index = process.argv.indexOf(name);
120
+ return index === -1 ? "" : process.argv[index + 1];
121
+ }
122
+ function readJson(filePath, fallback) {
123
+ if (!existsSync(filePath)) return fallback;
124
+ return JSON.parse(readFileSync(filePath, "utf8"));
125
+ }
126
+ function isDirectRun() {
127
+ return process.argv[1] && path.resolve(process.argv[1]) === __filename;
128
+ }
129
+ function resolveRepoRoot(startDir) {
130
+ const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
131
+ for (const candidate of candidates) if (existsSync(path.join(candidate, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
132
+ return path.resolve(startDir, "..");
133
+ }
@@ -0,0 +1,116 @@
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 scopePath = path.join(repoRoot, ".agents", "schemas", "artifact-scope.json");
9
+ const errors = [];
10
+
11
+ const scope = readJson(scopePath, "artifact scope");
12
+ const artifactTypes = scope.artifact_types || {};
13
+
14
+ for (const [artifactType, contract] of Object.entries(artifactTypes)) {
15
+ const schemaPath = contract.schema_path;
16
+ if (typeof schemaPath !== "string" || schemaPath.trim() === "") {
17
+ errors.push(`${artifactType} must declare schema_path in artifact-scope.json.`);
18
+ continue;
19
+ }
20
+
21
+ const absoluteSchemaPath = path.join(repoRoot, schemaPath);
22
+ const schema = readJson(absoluteSchemaPath, `${artifactType} schema`);
23
+ if (!existsSync(absoluteSchemaPath)) {
24
+ continue;
25
+ }
26
+
27
+ checkSchemaBasics(artifactType, schemaPath, schema);
28
+ checkRequiredFields(artifactType, contract, schema);
29
+ checkEnumFields(artifactType, contract, schema);
30
+ }
31
+
32
+ if (errors.length > 0) {
33
+ console.error("Artifact schema check failed:");
34
+ for (const error of errors) {
35
+ console.error(`- ${error}`);
36
+ }
37
+ process.exit(1);
38
+ }
39
+
40
+ console.log(`Artifact schema check passed for ${Object.keys(artifactTypes).length} artifact schemas.`);
41
+
42
+ function readJson(filePath, label) {
43
+ try {
44
+ return JSON.parse(readFileSync(filePath, "utf8"));
45
+ } catch (error) {
46
+ errors.push(`Could not read ${label} at ${toRepoPath(filePath)}: ${error.message}`);
47
+ return {};
48
+ }
49
+ }
50
+
51
+ function checkSchemaBasics(artifactType, schemaPath, schema) {
52
+ if (!schema.$schema) {
53
+ errors.push(`${schemaPath} must declare $schema.`);
54
+ }
55
+ if (!schema.$id) {
56
+ errors.push(`${schemaPath} must declare $id.`);
57
+ }
58
+ if (schema.type !== "object") {
59
+ errors.push(`${schemaPath} must describe an object schema.`);
60
+ }
61
+ if (!schema.title || !schema.title.toLowerCase().includes("delano")) {
62
+ errors.push(`${schemaPath} must include a Delano-specific title.`);
63
+ }
64
+ if (artifactType !== path.basename(schemaPath, ".schema.json")) {
65
+ errors.push(`${schemaPath} file name must match artifact type ${artifactType}.`);
66
+ }
67
+ }
68
+
69
+ function checkRequiredFields(artifactType, contract, schema) {
70
+ const expected = contract.required_fields || [];
71
+ const actual = schema.required || [];
72
+ for (const field of expected) {
73
+ if (!actual.includes(field)) {
74
+ errors.push(`${artifactType} schema must require canonical field: ${field}`);
75
+ }
76
+ if (!schema.properties || !schema.properties[field]) {
77
+ errors.push(`${artifactType} schema must define canonical property: ${field}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ function checkEnumFields(artifactType, contract, schema) {
83
+ for (const [field, expectedValues] of Object.entries(contract.enum_fields || {})) {
84
+ const property = schema.properties && schema.properties[field];
85
+ if (!property) {
86
+ errors.push(`${artifactType} schema must define enum property: ${field}`);
87
+ continue;
88
+ }
89
+
90
+ const actualValues = property.enum || [];
91
+ for (const value of expectedValues) {
92
+ if (!actualValues.includes(value)) {
93
+ errors.push(`${artifactType}.${field} schema enum missing value: ${value}`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ function resolveRepoRoot(startDir) {
100
+ const candidates = [
101
+ path.resolve(startDir, ".."),
102
+ path.resolve(startDir, "..", "..")
103
+ ];
104
+
105
+ for (const candidate of candidates) {
106
+ if (existsSync(path.join(candidate, ".agents", "schemas", "artifact-scope.json"))) {
107
+ return candidate;
108
+ }
109
+ }
110
+
111
+ return path.resolve(startDir, "..");
112
+ }
113
+
114
+ function toRepoPath(filePath) {
115
+ return path.relative(repoRoot, filePath).split(path.sep).join("/");
116
+ }
@@ -0,0 +1,23 @@
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); const __dirname = path.dirname(__filename);
5
+ const repoRoot = resolveRepoRoot(__dirname);
6
+ const errors = [];
7
+ const schema = readJson(path.join(repoRoot, ".agents", "schemas", "learning", "closeout-learning-proposal.schema.json"));
8
+ const skill = readText(path.join(repoRoot, ".agents", "skills", "closeout-skill", "SKILL.md"));
9
+ const runbook = readText(path.join(repoRoot, ".agents", "skills", "closeout-skill", "references", "runbook.md"));
10
+ const checklist = readText(path.join(repoRoot, ".agents", "skills", "closeout-skill", "templates", "closure-checklist.md"));
11
+ const template = readText(path.join(repoRoot, ".agents", "skills", "closeout-skill", "templates", "learning-proposal.md"));
12
+ for (const field of ["schema_version", "project", "task_ids", "proposal_type", "title", "rationale", "target_paths", "review_gate", "adoption_status"]) if (!schema.required?.includes(field)) errors.push(`proposal schema missing required field: ${field}`);
13
+ for (const type of ["rule", "skill", "schema", "fixture"]) if (!schema.properties?.proposal_type?.enum?.includes(type)) errors.push(`proposal schema missing proposal type: ${type}`);
14
+ if (!schema.properties?.review_gate?.enum?.includes("required-before-adoption")) errors.push("proposal schema must require review before adoption");
15
+ if (schema.properties?.target_paths?.items?.not?.pattern !== "^/") errors.push("proposal target paths must reject absolute paths");
16
+ for (const text of [skill, runbook, checklist, template]) {
17
+ if (!/review/i.test(text) || !/adoption/i.test(text)) errors.push("closeout learning workflow must mention review before adoption in skill assets");
18
+ }
19
+ if (errors.length) { console.error("Closeout learning proposal check failed:"); for (const error of errors) console.error(`- ${error}`); process.exit(1); }
20
+ console.log("Closeout learning proposal workflow check passed.");
21
+ function readJson(filePath){ if(!existsSync(filePath)){ errors.push(`missing file: ${path.relative(repoRoot,filePath)}`); return {}; } return JSON.parse(readFileSync(filePath,"utf8")); }
22
+ function readText(filePath){ if(!existsSync(filePath)){ errors.push(`missing file: ${path.relative(repoRoot,filePath)}`); return ""; } return readFileSync(filePath,"utf8"); }
23
+ 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,61 @@
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 contextDir = readOption("--context") || path.join(repoRoot, ".project", "context");
9
+ const requiredFiles = [
10
+ "project-overview.md",
11
+ "project-brief.md",
12
+ "tech-context.md",
13
+ "project-structure.md",
14
+ "system-patterns.md",
15
+ "product-context.md",
16
+ "project-style-guide.md",
17
+ "progress.md",
18
+ "gui-testing.md"
19
+ ];
20
+ const commandFiles = new Map([
21
+ ["project-style-guide.md", ["bash .agents/scripts/pm/validate.sh"]],
22
+ ["tech-context.md", ["validate.sh"]]
23
+ ]);
24
+ const results = requiredFiles.map(auditFile);
25
+ const summary = results.reduce((acc, item) => {
26
+ acc[item.classification] = (acc[item.classification] || 0) + 1;
27
+ return acc;
28
+ }, {});
29
+ const blocking = results.filter((item) => ["missing", "placeholder", "missing_required_commands"].includes(item.classification));
30
+ const report = { schema_version: 1, context_dir: path.relative(repoRoot, contextDir) || ".", file_count: results.length, summary, results, blocking_count: blocking.length };
31
+
32
+ if (process.argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
33
+ else console.log(`Context audit scored ${results.length} file(s): ${Object.entries(summary).map(([k,v])=>`${k}=${v}`).join(", ")}.`);
34
+ if (blocking.length) process.exit(1);
35
+
36
+ function auditFile(file) {
37
+ const filePath = path.join(contextDir, file);
38
+ if (!existsSync(filePath)) return { file, classification: "missing", score: 0, reasons: ["required context file is absent"] };
39
+ const text = readFileSync(filePath, "utf8");
40
+ const reasons = [];
41
+ if (isPlaceholder(text)) return { file, classification: "placeholder", score: 10, reasons: ["contains placeholder language or too little repo-specific content"] };
42
+ const requiredCommands = commandFiles.get(file) || [];
43
+ const missingCommands = requiredCommands.filter((command) => !text.includes(command));
44
+ if (missingCommands.length) return { file, classification: "missing_required_commands", score: 50, reasons: missingCommands.map((command)=>`missing command reference: ${command}`) };
45
+ if (file === "gui-testing.md") return { file, classification: "not_applicable", score: 100, reasons: ["advisory-only GUI policy file"] };
46
+ if (isStale(text)) reasons.push("frontmatter updated date is older than the current delivery cycle");
47
+ return { file, classification: reasons.length ? "stale" : "real", score: reasons.length ? 70 : 100, reasons };
48
+ }
49
+ function isPlaceholder(text) {
50
+ const compact = text.replace(/---[\s\S]*?---/, "").trim();
51
+ if (compact.length < 80) return true;
52
+ if (/TODO|TBD/i.test(compact)) return true;
53
+ const repoSpecific = /Delano|\.agents|\.project|HANDBOOK|npm|CLI|runtime/i.test(compact);
54
+ return !repoSpecific && /Capture architecture|Document major repository boundaries/i.test(compact);
55
+ }
56
+ function isStale(text) {
57
+ const match = text.match(/^updated:\s*(\d{4}-\d{2}-\d{2})/m);
58
+ return Boolean(match && match[1] < "2026-04-29");
59
+ }
60
+ function readOption(name) { const i = process.argv.indexOf(name); return i === -1 ? "" : process.argv[i + 1]; }
61
+ function resolveRepoRoot(startDir) { for (const c of [path.resolve(startDir,".."),path.resolve(startDir,"..","..")]) if (existsSync(path.join(c,".project"))) return c; return path.resolve(startDir,".."); }
@@ -0,0 +1,35 @@
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 schemaPath = path.join(repoRoot, ".agents", "schemas", "metrics", "delivery-event.schema.json");
9
+ const errors = [];
10
+ const schema = readJson(schemaPath);
11
+
12
+ const requiredFields = ["schema_version", "event_id", "event_type", "project", "created_at", "source", "privacy", "summary"];
13
+ const eventTypes = ["task-status-change", "validation-run", "sync-drift", "evidence-gap", "blocked-time", "closeout-learning"];
14
+ for (const field of requiredFields) if (!schema.required?.includes(field)) errors.push(`delivery metric event schema must require ${field}`);
15
+ for (const eventType of eventTypes) if (!schema.properties?.event_type?.enum?.includes(eventType)) errors.push(`delivery metric event type missing ${eventType}`);
16
+ if (schema.properties?.privacy?.properties?.raw_text_allowed?.const !== false) errors.push("delivery metric events must disallow raw text by default");
17
+ if (schema.properties?.privacy?.properties?.summary_only?.const !== true) errors.push("delivery metric events must be summary-only");
18
+ if (!schema.properties?.summary?.minLength) errors.push("delivery metric events must include a non-empty privacy-safe summary");
19
+
20
+ if (errors.length) {
21
+ console.error("Delivery metric event contract check failed:");
22
+ for (const error of errors) console.error(`- ${error}`);
23
+ process.exit(1);
24
+ }
25
+ console.log(`Delivery metric event schema check passed for ${eventTypes.length} event type(s).`);
26
+ console.log(`Delivery metric event contract check passed for ${eventTypes.length} event type(s).`);
27
+
28
+ function readJson(filePath) {
29
+ if (!existsSync(filePath)) {
30
+ errors.push(`missing file: ${path.relative(repoRoot, filePath)}`);
31
+ return {};
32
+ }
33
+ return JSON.parse(readFileSync(filePath, "utf8"));
34
+ }
35
+ 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,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 schema = readJson(path.join(repoRoot, ".agents", "schemas", "metrics", "delivery-events.schema.json"));
10
+ const docs = readText(path.join(repoRoot, ".agents", "logs", "delivery-metrics.md"));
11
+
12
+ for (const field of ["schema_version", "event_id", "event_type", "timestamp", "project", "actor", "summary"]) {
13
+ if (!schema.required?.includes(field)) errors.push(`delivery metric schema missing required field: ${field}`);
14
+ }
15
+ const eventTypes = schema.properties?.event_type?.enum || [];
16
+ for (const eventType of ["task_status_changed", "validation_run", "lease_acquired", "lease_released", "drift_report_generated", "repair_plan_created", "closeout_recorded"]) {
17
+ if (!eventTypes.includes(eventType)) errors.push(`delivery metric schema missing event type: ${eventType}`);
18
+ if (!docs.includes(eventType)) errors.push(`delivery metrics docs missing event type: ${eventType}`);
19
+ }
20
+ if (schema.properties?.summary?.properties?.privacy?.const !== "metadata-only") errors.push("delivery metric summaries must be metadata-only.");
21
+ const repoPathPattern = schema.properties?.evidence?.properties?.repo_paths?.items?.not?.pattern;
22
+ if (repoPathPattern !== "^/") errors.push("delivery metric evidence must reject absolute repo paths.");
23
+ for (const forbidden of ["prompt_raw", "prompt_redacted", "transcript", "customer_data"]) {
24
+ if (JSON.stringify(schema).includes(forbidden) || docs.includes(forbidden)) errors.push(`delivery metrics contract mentions forbidden raw field: ${forbidden}`);
25
+ }
26
+
27
+ if (errors.length > 0) {
28
+ console.error("Delivery metric event check failed:");
29
+ for (const error of errors) console.error(`- ${error}`);
30
+ process.exit(1);
31
+ }
32
+ console.log(`Delivery metric event check passed for ${eventTypes.length} event type(s).`);
33
+
34
+ function readJson(filePath) {
35
+ if (!existsSync(filePath)) {
36
+ errors.push(`missing file: ${path.relative(repoRoot, filePath)}`);
37
+ return {};
38
+ }
39
+ return JSON.parse(readFileSync(filePath, "utf8"));
40
+ }
41
+ function readText(filePath) {
42
+ if (!existsSync(filePath)) {
43
+ errors.push(`missing file: ${path.relative(repoRoot, filePath)}`);
44
+ return "";
45
+ }
46
+ return readFileSync(filePath, "utf8");
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, ".project")) && existsSync(path.join(candidate, ".agents"))) return candidate;
51
+ return path.resolve(startDir, "..");
52
+ }