@bvdm/delano 0.2.3 → 0.2.5

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 (40) hide show
  1. package/.delano/viewer/README.md +3 -2
  2. package/.delano/viewer/public/app.js +13 -1
  3. package/.delano/viewer/public/app.jsx +2312 -0
  4. package/.delano/viewer/public/delano-mark.svg +4 -0
  5. package/.delano/viewer/public/index.html +12 -14
  6. package/.delano/viewer/public/styles.css +1005 -833
  7. package/.delano/viewer/server.js +46 -5
  8. package/README.md +63 -3
  9. package/assets/install-manifest.json +7 -0
  10. package/assets/payload/.agents/adapters/manifest.schema.json +103 -0
  11. package/assets/payload/.agents/adapters/spec-kit/adapter.json +71 -0
  12. package/assets/payload/.agents/hooks/README.md +6 -1
  13. package/assets/payload/.agents/hooks/codex-session-status.js +123 -0
  14. package/assets/payload/.agents/schemas/status-transitions.json +35 -0
  15. package/assets/payload/.agents/scripts/README.md +1 -1
  16. package/assets/payload/.agents/scripts/check-status-transitions.mjs +171 -2
  17. package/assets/payload/.agents/scripts/pm/import-spec-kit.sh +605 -0
  18. package/assets/payload/.agents/scripts/pm/init.sh +31 -2
  19. package/assets/payload/.agents/scripts/pm/research.sh +296 -0
  20. package/assets/payload/.agents/scripts/pm/status.sh +135 -28
  21. package/assets/payload/.agents/scripts/pm/validate.sh +16 -0
  22. package/assets/payload/.codex/hooks.json +17 -0
  23. package/assets/payload/.delano/viewer/README.md +3 -2
  24. package/assets/payload/.delano/viewer/public/app.js +13 -1
  25. package/assets/payload/.delano/viewer/public/index.html +12 -14
  26. package/assets/payload/.delano/viewer/public/styles.css +1005 -833
  27. package/assets/payload/.delano/viewer/server.js +46 -5
  28. package/assets/payload/.project/templates/decisions.md +18 -0
  29. package/assets/payload/.project/templates/plan.md +17 -0
  30. package/assets/payload/.project/templates/spec.md +12 -0
  31. package/assets/payload/.project/templates/task.md +6 -0
  32. package/assets/payload/.project/templates/workstream.md +1 -0
  33. package/package.json +4 -2
  34. package/src/cli/commands/install.js +2 -1
  35. package/src/cli/commands/state.js +689 -0
  36. package/src/cli/commands/viewer.js +2 -1
  37. package/src/cli/commands/wrapper.js +29 -5
  38. package/src/cli/index.js +120 -7
  39. package/src/cli/lib/install.js +179 -2
  40. package/src/cli/lib/project-state.js +918 -0
@@ -1,14 +1,20 @@
1
+ const path = require("node:path");
2
+
1
3
  const { runPmScript } = require("../lib/pm");
2
4
 
3
- function createWrapperCommand(scriptName) {
5
+ function createWrapperCommand(scriptName, options = {}) {
4
6
  return {
5
- description: `Run .agents/scripts/pm/${scriptName}.sh in the current Delano repository.`,
7
+ description: options.description || `Run .agents/scripts/pm/${scriptName}.sh in the current Delano repository.`,
6
8
  run(args) {
7
9
  const passthrough = args[0] === "--" ? args.slice(1) : args;
8
- return runPmScript(scriptName, passthrough);
10
+ return runPmScript(scriptName, normalizePassthrough(scriptName, passthrough));
9
11
  },
10
12
  help() {
11
- return [
13
+ if (typeof options.help === "function") {
14
+ return options.help();
15
+ }
16
+
17
+ const lines = [
12
18
  "Usage:",
13
19
  ` delano ${scriptName} [-- <script-args>]`,
14
20
  "",
@@ -16,11 +22,29 @@ function createWrapperCommand(scriptName) {
16
22
  ` - Resolves the current Delano repository by searching upward for .project/ and .agents/scripts/pm/.`,
17
23
  ` - Runs .agents/scripts/pm/${scriptName}.sh through bash.`,
18
24
  " - Pass '--' to make argument passthrough explicit when needed."
19
- ].join("\n");
25
+ ];
26
+
27
+ if (scriptName === "status") {
28
+ lines.push(
29
+ "",
30
+ "Status examples:",
31
+ " delano status --open --brief",
32
+ " delano status -- --open --brief"
33
+ );
34
+ }
35
+
36
+ return lines.join("\n");
20
37
  }
21
38
  };
22
39
  }
23
40
 
41
+ function normalizePassthrough(scriptName, args) {
42
+ if (scriptName !== "import-spec-kit" || args.length < 2 || path.isAbsolute(args[1])) {
43
+ return args;
44
+ }
45
+ return [args[0], path.resolve(process.cwd(), args[1]), ...args.slice(2)];
46
+ }
47
+
24
48
  module.exports = {
25
49
  createWrapperCommand
26
50
  };
package/src/cli/index.js CHANGED
@@ -7,9 +7,28 @@ const { getOnboardingHelp, runOnboarding } = require("./commands/onboarding");
7
7
  const { runInstall, getInstallHelp } = require("./commands/install");
8
8
  const { runViewer, getViewerHelp } = require("./commands/viewer");
9
9
  const { createWrapperCommand } = require("./commands/wrapper");
10
+ const {
11
+ getProjectHelp,
12
+ getTaskHelp,
13
+ getUpdateHelp,
14
+ getWorkstreamHelp,
15
+ parseTaskArgs,
16
+ runProjectCommand,
17
+ runTaskCommand,
18
+ runUpdateCommand,
19
+ runWorkstreamCommand
20
+ } = require("./commands/state");
10
21
 
11
22
  const wrapperCommands = {
12
23
  init: createWrapperCommand("init"),
24
+ "import-spec-kit": createWrapperCommand("import-spec-kit", {
25
+ description: "Create a planned Delano project from a Spec Kit-style markdown artifact.",
26
+ help: getImportSpecKitHelp
27
+ }),
28
+ research: createWrapperCommand("research", {
29
+ description: "Create repo-native research intake files for a Delano project.",
30
+ help: getResearchHelp
31
+ }),
13
32
  validate: createWrapperCommand("validate"),
14
33
  status: createWrapperCommand("status"),
15
34
  next: createWrapperCommand("next")
@@ -31,18 +50,36 @@ const commands = {
31
50
  run: runViewer,
32
51
  help: getViewerHelp
33
52
  },
53
+ project: {
54
+ description: "Create, show, and patch Delano project contracts.",
55
+ run: runProjectCommand,
56
+ help: getProjectHelp
57
+ },
58
+ workstream: {
59
+ description: "Add and patch Delano workstream contracts.",
60
+ run: runWorkstreamCommand,
61
+ help: getWorkstreamHelp
62
+ },
63
+ task: {
64
+ description: "Add and patch Delano task contracts with scoped lifecycle rollups.",
65
+ run: runTaskCommand,
66
+ help: getTaskHelp
67
+ },
68
+ update: {
69
+ description: "Add project progress updates from the project update template.",
70
+ run: runUpdateCommand,
71
+ help: getUpdateHelp
72
+ },
34
73
  init: wrapperCommands.init,
74
+ "import-spec-kit": wrapperCommands["import-spec-kit"],
75
+ research: wrapperCommands.research,
35
76
  validate: wrapperCommands.validate,
36
77
  status: wrapperCommands.status,
37
78
  next: wrapperCommands.next
38
79
  };
39
80
 
40
81
  function isInstallShorthand(argv) {
41
- if (argv.length === 0) {
42
- return false;
43
- }
44
-
45
- return argv[0].startsWith("-");
82
+ return argv.length > 0 && argv[0].startsWith("-");
46
83
  }
47
84
 
48
85
  function resolveInvocation(argv) {
@@ -99,8 +136,14 @@ function getGeneralHelp() {
99
136
  " onboarding Analyze AGENTS.md with the approval-first onboarding skill",
100
137
  " install Install the approved Delano runtime payload",
101
138
  " viewer Launch the read-only local UI for .project contracts",
102
- " init Run .agents/scripts/pm/init.sh in the current Delano repo",
103
- " validate Run .agents/scripts/pm/validate.sh in the current Delano repo",
139
+ " project Create, show, and patch project contracts",
140
+ " workstream Add and patch workstream contracts",
141
+ " task Add and patch task contracts with scoped lifecycle rollups",
142
+ " update Add a progress update from .project/templates",
143
+ " init Run .agents/scripts/pm/init.sh in the current Delano repo",
144
+ " import-spec-kit Create a planned project from a Spec Kit-style markdown artifact",
145
+ " research Create repo-native research intake files for a project",
146
+ " validate Run .agents/scripts/pm/validate.sh in the current Delano repo",
104
147
  " status Run .agents/scripts/pm/status.sh in the current Delano repo",
105
148
  " next Run .agents/scripts/pm/next.sh in the current Delano repo",
106
149
  "",
@@ -115,7 +158,16 @@ function getGeneralHelp() {
115
158
  " delano --target ../my-repo --yes",
116
159
  " npx -y @bvdm/delano@latest --yes",
117
160
  " delano viewer",
161
+ " delano project create my-project --name \"My Project\" --owner team",
162
+ " delano workstream add my-project WS-A --name \"API Foundation\" --owner backend-team",
163
+ " delano task add my-project T-001 --name \"Build endpoint\" --workstream WS-A",
164
+ " delano task start my-project T-001",
165
+ " delano task close my-project T-001 --evidence \"Implemented and tested\"",
166
+ " delano update add my-project --message \"Implemented endpoint smoke test\"",
167
+ " delano import-spec-kit reminder-email-preferences docs/spec-kit/fixtures/minimal-spec-kit-project.md --json",
168
+ " delano research my-project api-options --title \"API options\" --question \"Which API shape should we use?\" --json",
118
169
  " delano validate",
170
+ " delano status --open --brief",
119
171
  " delano next -- --all",
120
172
  "",
121
173
  "Shorthand:",
@@ -128,6 +180,63 @@ function getGeneralHelp() {
128
180
  ].join("\n");
129
181
  }
130
182
 
183
+ function getResearchHelp() {
184
+ return [
185
+ "Usage:",
186
+ " delano research <project-slug> <research-slug> [options]",
187
+ "",
188
+ "Creates repo-native research intake files under .project/projects/<project-slug>/research/<research-slug>/, then runs Delano validation by default.",
189
+ "",
190
+ "Arguments:",
191
+ " project-slug Existing Delano project slug",
192
+ " research-slug Research folder slug in kebab-case",
193
+ "",
194
+ "Options:",
195
+ " --title <title> Human-readable research title",
196
+ " --question <question> Primary research question",
197
+ " --owner <owner> Research owner, defaults to team",
198
+ " --no-validate Create artifacts without running Delano validation",
199
+ " --json Print a single machine-readable JSON result",
200
+ " -h, --help Show help",
201
+ "",
202
+ "Agent examples:",
203
+ " delano research delano-spec-kit-interop import-edge-cases --title \"Import edge cases\" --question \"Which inputs should block import?\" --json",
204
+ "",
205
+ "Output:",
206
+ " Human mode prints a concise summary plus validation output.",
207
+ " JSON mode prints: { ok, command, project, research, files, validation }."
208
+ ].join("\n");
209
+ }
210
+
211
+ function getImportSpecKitHelp() {
212
+ return [
213
+ "Usage:",
214
+ " delano import-spec-kit <slug> <source-md> [options]",
215
+ "",
216
+ "Creates a planned Delano project from the first supported Spec Kit-style markdown fixture, then runs Delano validation by default.",
217
+ "",
218
+ "Arguments:",
219
+ " slug Target Delano project slug in kebab-case",
220
+ " source-md Path to the markdown source artifact",
221
+ "",
222
+ "Options:",
223
+ " --name <project-name> Project name override",
224
+ " --owner <owner> Project owner, defaults to team",
225
+ " --lead <lead> Project lead, defaults to owner",
226
+ " --no-validate Create artifacts without running Delano validation",
227
+ " --json Print a single machine-readable JSON result",
228
+ " -h, --help Show help",
229
+ "",
230
+ "Agent examples:",
231
+ " delano import-spec-kit reminder-email-preferences docs/spec-kit/fixtures/minimal-spec-kit-project.md --json",
232
+ " delano import-spec-kit reminder-email-preferences input.md --name \"Reminder Email Preferences\" --owner platform --lead clark --json",
233
+ "",
234
+ "Output:",
235
+ " Human mode prints a concise summary plus validation output.",
236
+ " JSON mode prints: { ok, command, project, source, validation }."
237
+ ].join("\n");
238
+ }
239
+
131
240
  async function run(argv) {
132
241
  const invocation = resolveInvocation(argv);
133
242
 
@@ -155,6 +264,10 @@ async function run(argv) {
155
264
  module.exports = {
156
265
  commands,
157
266
  getGeneralHelp,
267
+ getImportSpecKitHelp,
268
+ getResearchHelp,
269
+ getTaskHelp,
270
+ parseTaskArgs,
158
271
  resolveInvocation,
159
272
  run
160
273
  };
@@ -6,6 +6,7 @@ const {
6
6
  readFileSync,
7
7
  rmSync,
8
8
  statSync,
9
+ writeFileSync,
9
10
  } = require("node:fs");
10
11
  const path = require("node:path");
11
12
  const readline = require("node:readline");
@@ -14,8 +15,16 @@ const { stdin, stdout } = require("node:process");
14
15
  const { CliError } = require("./errors");
15
16
  const { getPackageRoot, getPathType } = require("./runtime");
16
17
 
18
+ const CODEX_HOOKS_TARGET = ".codex/hooks.json";
19
+ const CODEX_SESSION_STATUS_SCRIPT = ".agents/hooks/codex-session-status.js";
20
+
17
21
  const SUPPORTED_AGENTS = ["claude", "codex", "opencode", "pi"];
18
22
  const INSTALL_CATEGORIES = [
23
+ {
24
+ name: "codex-hooks",
25
+ description: ".codex hook configuration and SessionStart shim",
26
+ matches: (target) => target.startsWith(".codex/") || target === CODEX_SESSION_STATUS_SCRIPT
27
+ },
19
28
  {
20
29
  name: "agent-runtime",
21
30
  description: ".agents runtime except skills",
@@ -67,6 +76,8 @@ const INSTALL_CATEGORY_ALIASES = new Map([
67
76
  ["agent-skills", "skills"],
68
77
  ["agents", "agent-runtime"],
69
78
  ["runtime", "agent-runtime"],
79
+ ["codex", "codex-hooks"],
80
+ ["codex-config", "codex-hooks"],
70
81
  ["context", "project-context"],
71
82
  ["templates", "project-templates"],
72
83
  ["project-state", "project-projects"],
@@ -531,6 +542,9 @@ function collectConflicts(plan) {
531
542
 
532
543
  const exactType = getPathType(item.targetPath);
533
544
  if (exactType) {
545
+ if (isNonBlockingExistingTarget(item.relativePath)) {
546
+ continue;
547
+ }
534
548
  conflicts.push({
535
549
  relativePath: item.relativePath,
536
550
  conflictPath: item.relativePath,
@@ -566,6 +580,7 @@ function printPlanSummary(plan, options) {
566
580
  console.log(`Force: ${options.force ? "yes" : "no"}`);
567
581
  console.log("");
568
582
  console.log("Note: --agents is accepted now for forward compatibility, but v1 base install still excludes top-level adapter entry docs by default.");
583
+ console.log("Note: .codex/hooks.json is merged when it already exists, and Codex runs the hook only after hooks are enabled and trusted.");
569
584
  console.log("Note: .project/context, .project/projects, and .project/registry are repo-owned after install; use --no-project-state or --only for update-safe refreshes.");
570
585
  }
571
586
 
@@ -605,7 +620,20 @@ async function confirmInstall(plan, options) {
605
620
  }
606
621
 
607
622
  function applyInstallPlan(plan, options) {
623
+ let appliedCount = 0;
624
+ let skippedCount = 0;
625
+
608
626
  for (const item of plan.items) {
627
+ if (item.relativePath === CODEX_HOOKS_TARGET) {
628
+ const result = applyCodexHooksConfig(item);
629
+ if (result === "skipped") {
630
+ skippedCount += 1;
631
+ } else {
632
+ appliedCount += 1;
633
+ }
634
+ continue;
635
+ }
636
+
609
637
  const existingType = getPathType(item.targetPath);
610
638
  if (existingType) {
611
639
  rmSync(item.targetPath, { recursive: true, force: true });
@@ -619,13 +647,161 @@ function applyInstallPlan(plan, options) {
619
647
  } catch {
620
648
  // Ignore mode-setting failures on platforms that do not preserve POSIX modes.
621
649
  }
650
+ appliedCount += 1;
622
651
  }
623
652
 
624
653
  console.log("");
625
- console.log(`Installed ${plan.items.length} files into ${options.target}.`);
654
+ console.log(`Installed or updated ${appliedCount} files into ${options.target}.`);
655
+ if (skippedCount > 0) {
656
+ console.log(`Skipped ${skippedCount} non-blocking file(s).`);
657
+ }
658
+ if (plan.items.some((item) => item.relativePath === CODEX_HOOKS_TARGET)) {
659
+ console.log("");
660
+ console.log("Codex hook config installed or merged at .codex/hooks.json.");
661
+ console.log("To activate it, enable Codex hooks, then approve the project and hook trust prompts.");
662
+ console.log("For one session, run: codex --enable hooks");
663
+ console.log("For persistent config, set [features].hooks = true in ~/.codex/config.toml.");
664
+ }
626
665
  console.log("Recommended next step: run 'delano onboarding' to review AGENTS.md. The command asks for explicit approval before analysis.");
627
666
  }
628
667
 
668
+ function isNonBlockingExistingTarget(relativePath) {
669
+ return relativePath === CODEX_HOOKS_TARGET;
670
+ }
671
+
672
+ function applyCodexHooksConfig(item) {
673
+ const existingType = getPathType(item.targetPath);
674
+ if (!existingType) {
675
+ mkdirSync(path.dirname(item.targetPath), { recursive: true });
676
+ copyFileSync(item.sourcePath, item.targetPath);
677
+ applySourceMode(item.sourcePath, item.targetPath);
678
+ return "installed";
679
+ }
680
+
681
+ if (existingType !== "file") {
682
+ console.warn(`Skipped ${CODEX_HOOKS_TARGET}: existing ${existingType} cannot be merged safely.`);
683
+ return "skipped";
684
+ }
685
+
686
+ let existingConfig;
687
+ try {
688
+ existingConfig = readJsonFile(item.targetPath);
689
+ } catch (error) {
690
+ console.warn(`Skipped ${CODEX_HOOKS_TARGET}: existing file is not valid JSON (${error.message}).`);
691
+ return "skipped";
692
+ }
693
+
694
+ const packagedConfig = readJsonFile(item.sourcePath);
695
+ const mergeResult = mergeCodexHooksConfig(existingConfig, packagedConfig);
696
+ if (!mergeResult.ok) {
697
+ console.warn(`Skipped ${CODEX_HOOKS_TARGET}: ${mergeResult.reason}`);
698
+ return "skipped";
699
+ }
700
+
701
+ if (mergeResult.changed) {
702
+ writeFileSync(item.targetPath, `${JSON.stringify(mergeResult.config, null, 2)}\n`, "utf8");
703
+ return "merged";
704
+ }
705
+
706
+ return "unchanged";
707
+ }
708
+
709
+ function mergeCodexHooksConfig(existingConfig, packagedConfig) {
710
+ if (!isPlainObject(existingConfig)) {
711
+ return { ok: false, reason: "existing config must be a JSON object." };
712
+ }
713
+ if (!isPlainObject(packagedConfig) || !isPlainObject(packagedConfig.hooks)) {
714
+ throw new CliError("Packaged .codex/hooks.json is missing a hooks object.", 1);
715
+ }
716
+
717
+ const packagedSessionStart = packagedConfig.hooks.SessionStart;
718
+ if (!Array.isArray(packagedSessionStart)) {
719
+ throw new CliError("Packaged .codex/hooks.json is missing hooks.SessionStart.", 1);
720
+ }
721
+
722
+ const nextConfig = deepClone(existingConfig);
723
+ if (nextConfig.hooks === undefined) {
724
+ nextConfig.hooks = {};
725
+ }
726
+ if (!isPlainObject(nextConfig.hooks)) {
727
+ return { ok: false, reason: "existing hooks field must be a JSON object." };
728
+ }
729
+ if (nextConfig.hooks.SessionStart === undefined) {
730
+ nextConfig.hooks.SessionStart = [];
731
+ }
732
+ if (!Array.isArray(nextConfig.hooks.SessionStart)) {
733
+ return { ok: false, reason: "existing hooks.SessionStart field must be an array." };
734
+ }
735
+
736
+ let changed = false;
737
+ for (const desiredGroup of packagedSessionStart) {
738
+ if (!hasDelanoSessionStatusHook(nextConfig.hooks.SessionStart, desiredGroup)) {
739
+ nextConfig.hooks.SessionStart.push(deepClone(desiredGroup));
740
+ changed = true;
741
+ }
742
+ }
743
+
744
+ return {
745
+ ok: true,
746
+ changed,
747
+ config: nextConfig
748
+ };
749
+ }
750
+
751
+ function hasDelanoSessionStatusHook(sessionStartGroups, desiredGroup) {
752
+ const desiredCommands = collectHookCommands([desiredGroup]);
753
+ for (const group of sessionStartGroups) {
754
+ const commands = collectHookCommands([group]);
755
+ if (commands.some((command) => command.includes("codex-session-status.js"))) {
756
+ return true;
757
+ }
758
+ if (commands.some((command) => desiredCommands.includes(command))) {
759
+ return true;
760
+ }
761
+ }
762
+ return false;
763
+ }
764
+
765
+ function collectHookCommands(groups) {
766
+ const commands = [];
767
+ for (const group of groups) {
768
+ if (!group || !Array.isArray(group.hooks)) {
769
+ continue;
770
+ }
771
+ for (const hook of group.hooks) {
772
+ if (hook && typeof hook.command === "string") {
773
+ commands.push(hook.command);
774
+ }
775
+ }
776
+ }
777
+ return commands;
778
+ }
779
+
780
+ function applySourceMode(sourcePath, targetPath) {
781
+ const sourceMode = statSync(sourcePath).mode & 0o777;
782
+ try {
783
+ chmodSync(targetPath, sourceMode);
784
+ } catch {
785
+ // Ignore mode-setting failures on platforms that do not preserve POSIX modes.
786
+ }
787
+ }
788
+
789
+ function deepClone(value) {
790
+ return JSON.parse(JSON.stringify(value));
791
+ }
792
+
793
+ function isPlainObject(value) {
794
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
795
+ }
796
+
797
+ function readJsonFile(filePath) {
798
+ return JSON.parse(stripByteOrderMark(readFileSync(filePath, "utf8")));
799
+ }
800
+
801
+ function stripByteOrderMark(text) {
802
+ return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
803
+ }
804
+
629
805
  function normalizeManifestEntries(rawManifest) {
630
806
  const entries = Array.isArray(rawManifest.files) ? rawManifest.files : rawManifest.paths;
631
807
  if (!Array.isArray(entries)) {
@@ -686,5 +862,6 @@ module.exports = {
686
862
  printConflicts,
687
863
  printPlanSummary,
688
864
  readInstallManifest,
689
- getMissingPackagedAssetMessage
865
+ getMissingPackagedAssetMessage,
866
+ mergeCodexHooksConfig
690
867
  };