forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -4,22 +4,34 @@ const memoryRouter = require('../memory/router');
4
4
  const { stripGlobalFlags } = require('../global-flags');
5
5
  const { fenceUntrusted } = require('../untrusted-content');
6
6
 
7
- const usage = 'Usage: forge recall [query] [--limit N] [--all] [--json]';
7
+ const usage = 'Usage: forge recall [query] [--kind <type>] [--limit N] [--all] [--json]';
8
+
9
+ // Reserved tag prefix that `remember --kind` writes (kernel issue 8cc1db4d). A `--kind`
10
+ // filter keeps only notes carrying this tag; the prefix is stripped when surfacing the
11
+ // derived `type` field so a note's user tags stay clean. The filter FLAG is `--kind` (NOT
12
+ // `--type`): `--type` is a reserved GLOBAL flag hard-validated to workflow classifications.
13
+ const TYPE_TAG_PREFIX = 'type:';
14
+
15
+ // When a `--kind` filter is active the tag filter runs in the command layer (the store is
16
+ // not reimplemented), so scan a generous window of recent notes before filtering rather than
17
+ // the small default page — otherwise the type match could fall outside the default limit.
18
+ const TYPE_FILTER_SCAN = 1000;
8
19
 
9
20
  /**
10
- * Separate the optional positional query from `--limit N` and `--json`.
11
- * Global flags (e.g. `-p <dir>`, `--all`) are stripped first so they never
12
- * corrupt the search query (kernel issue c1e090ff). `--all` is a GLOBAL boolean
13
- * flag, so the handler reads it from its `flags` argument (or the raw args on a
14
- * direct call) not from this parsed query.
21
+ * Separate the optional positional query from `--kind <type>`, `--limit N`, and `--json`.
22
+ * Global flags (e.g. `-p <dir>`, `--all`) are stripped first so they never corrupt the
23
+ * search query (kernel issue c1e090ff). `--all` is a GLOBAL boolean flag, so the handler
24
+ * reads it from its `flags` argument (or the raw args on a direct call) — not from here.
25
+ * `--kind` is NOT a global flag, so it survives the strip and is parsed here.
15
26
  *
16
27
  * @param {string[]} rawArgs - Raw command arguments.
17
- * @returns {{ query: string, limit: (number|undefined), json: boolean }}
28
+ * @returns {{ query: string, limit: (number|undefined), type: (string|undefined), json: boolean }}
18
29
  */
19
30
  function parseArgs(rawArgs) {
20
31
  const args = stripGlobalFlags(rawArgs);
21
32
  const words = [];
22
33
  let limit;
34
+ let type;
23
35
  let json = false;
24
36
 
25
37
  for (let index = 0; index < args.length; index += 1) {
@@ -37,18 +49,43 @@ function parseArgs(rawArgs) {
37
49
  if (Number.isInteger(value) && value > 0) {
38
50
  limit = value;
39
51
  }
52
+ } else if (arg === '--kind') {
53
+ const value = args[index + 1];
54
+ if (value && !value.startsWith('--')) {
55
+ type = value.trim();
56
+ index += 1;
57
+ }
58
+ } else if (arg.startsWith('--kind=')) {
59
+ type = arg.slice('--kind='.length).trim();
40
60
  } else {
41
61
  words.push(arg);
42
62
  }
43
63
  }
44
64
 
45
- return { query: words.join(' ').trim(), limit, json };
65
+ return { query: words.join(' ').trim(), limit, type, json };
66
+ }
67
+
68
+ // Derive a note's `type` from its reserved `type:` tag (undefined when untyped), so any
69
+ // surface — JSON and text — can show and filter by kind without exposing the tag encoding.
70
+ function typeOf(entry) {
71
+ const tag = (entry.tags || []).find(t => t.startsWith(TYPE_TAG_PREFIX));
72
+ return tag ? tag.slice(TYPE_TAG_PREFIX.length) : undefined;
73
+ }
74
+
75
+ function withType(entry) {
76
+ const type = typeOf(entry);
77
+ return type ? { ...entry, type } : entry;
46
78
  }
47
79
 
48
80
  function formatEntry(entry) {
49
81
  const date = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
50
82
  const prefix = date ? `${date} ` : '';
51
- const tagSuffix = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : '';
83
+ // The reserved `type:` tag renders as a leading `(kind)` marker, not as a raw tag, so the
84
+ // displayed tags stay the user's own labels.
85
+ const type = typeOf(entry);
86
+ const userTags = (entry.tags || []).filter(t => !t.startsWith(TYPE_TAG_PREFIX));
87
+ const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
88
+ const typeMarker = type ? `(${type}) ` : '';
52
89
  // Machine/insights records are LABELED with their source so they are never mistaken for a
53
90
  // plain human note; human `remember` notes render clean.
54
91
  const marker = entry.machine && entry.sourceAgent ? `(${entry.sourceAgent}) ` : '';
@@ -56,16 +93,33 @@ function formatEntry(entry) {
56
93
  // so the human/agent-facing render is provenance-fenced. The `--json` path above
57
94
  // keeps the raw note so programmatic consumers/parsers are unaffected.
58
95
  const note = fenceUntrusted(entry.note, { source: 'memory' });
59
- return `- ${prefix}${marker}${note}${tagSuffix}`;
96
+ return `- ${prefix}${marker}${typeMarker}${note}${tagSuffix}`;
60
97
  }
61
98
 
62
99
  async function handler(args, flags, projectRoot) {
63
- const { query, limit, json } = parseArgs(args);
100
+ const { query, limit, type, json } = parseArgs(args);
64
101
  // `--all` is a GLOBAL boolean flag: in production bin/forge.js strips it from
65
102
  // args and sets flags.all; on a direct handler call it may still be in args.
66
103
  const all = Boolean(flags && flags.all) || args.includes('--all');
67
104
 
68
- const { notes, total, capped, scope } = memoryRouter.recall(projectRoot, { query, limit, all });
105
+ // A `--type` filter scans a generous recent window, then keeps only matching notes — the
106
+ // read stays entirely in the existing store (no schema change). `--limit` is re-applied
107
+ // AFTER filtering so it caps the typed result set, not the pre-filter scan.
108
+ const recallLimit = type ? Math.max(limit ?? 0, TYPE_FILTER_SCAN) : limit;
109
+ const result = memoryRouter.recall(projectRoot, { query, limit: recallLimit, all });
110
+ let notes = result.notes.map(withType);
111
+ let { total, capped } = result;
112
+ const { scope } = result;
113
+ if (type) {
114
+ notes = notes.filter(entry => entry.type === type);
115
+ total = notes.length;
116
+ if (limit && notes.length > limit) {
117
+ notes = notes.slice(0, limit);
118
+ capped = true;
119
+ } else {
120
+ capped = false;
121
+ }
122
+ }
69
123
 
70
124
  if (json) {
71
125
  // Object (not a bare array) so programmatic consumers see the total and whether the
@@ -108,6 +162,7 @@ module.exports = {
108
162
  description: 'Retrieve project-memory notes from the kernel-backed memory store',
109
163
  usage,
110
164
  flags: {
165
+ '--kind': 'Filter to notes of a type (decision|bugfix|gotcha|...); --type is reserved',
111
166
  '--limit': 'Cap the number of notes returned',
112
167
  '--all': 'Include machine/insights records in the no-query listing (query already searches all)',
113
168
  '--json': 'Emit machine-readable JSON output',
@@ -4,6 +4,7 @@ const {
4
4
  buildIssueRecap,
5
5
  formatOrientationText,
6
6
  } = require('../orientation');
7
+ const { recordContextLoaded } = require('../grounding/context-events');
7
8
 
8
9
  const usage = 'Usage: forge recap <issue> [--budget N] [--json]';
9
10
 
@@ -35,7 +36,7 @@ function readIssueArg(args) {
35
36
 
36
37
  // `forge recap <issue>` summarizes a single issue from the deterministic
37
38
  // orientation source assembly — it is issue-scoped, not a project-wide recap.
38
- async function handler(args, _flags, projectRoot) {
39
+ async function handler(args, _flags, projectRoot, opts = {}) {
39
40
  const issueId = readIssueArg(args);
40
41
  if (!issueId) {
41
42
  // Use `error` (not `output`) so the CLI dispatcher prints the usage line
@@ -44,9 +45,22 @@ async function handler(args, _flags, projectRoot) {
44
45
  return { success: false, error: usage };
45
46
  }
46
47
 
47
- const recap = buildIssueRecap(projectRoot, issueId, {
48
- budgetTokens: readOption(args, '--budget', undefined),
49
- });
48
+ const budget = readOption(args, '--budget', undefined);
49
+ const recap = buildIssueRecap(projectRoot, issueId, { budgetTokens: budget });
50
+
51
+ // Grounding (gate.read_first): a successful recap IS the load-the-doc action,
52
+ // so append a `context.loaded` event that unblocks a later `forge claim <id>`.
53
+ // Best-effort and awaited so the event is durable before the CLI process exits;
54
+ // a bookkeeping failure never fails the recap render.
55
+ const deps = (opts.kernelBroker && opts.kernelDriver)
56
+ ? { kernelBroker: opts.kernelBroker, kernelDriver: opts.kernelDriver }
57
+ : undefined;
58
+ try {
59
+ await recordContextLoaded(projectRoot, {
60
+ issueId, cmd: 'recap', budget, session: opts.session, env: opts.env, deps, now: opts.now,
61
+ });
62
+ } catch { /* best-effort: never fail a recap on grounding bookkeeping */ }
63
+
50
64
  return {
51
65
  success: true,
52
66
  output: args.includes('--json') ? `${JSON.stringify(recap, null, 2)}\n` : formatOrientationText(recap),
@@ -4,6 +4,7 @@ const {
4
4
  SUPPORTED_TARGET,
5
5
  buildReadinessReport,
6
6
  renderReadinessReport,
7
+ writeAuditArtifact,
7
8
  } = require('../release-readiness');
8
9
  const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
9
10
  const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = require('./_issue');
@@ -11,9 +12,8 @@ const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = requir
11
12
  // `forge release <id>` releases a claimed issue; `forge release check` runs the
12
13
  // release-readiness gate. The two share the top-level verb, so this command
13
14
  // dispatches `check` to the gate and routes everything else through the shared
14
- // issue dispatch (resolve backend → runIssueOperation('release') → normalize). The
15
- // Beads backend has no release op and returns the Kernel-only contract error.
16
- const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json]';
15
+ // issue dispatch (resolve backend → runIssueOperation('release') → normalize).
16
+ const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit';
17
17
 
18
18
  async function runReleaseIssue(args, projectRoot, opts = {}) {
19
19
  const resolved = withResolvedIssueBackend(projectRoot, opts);
@@ -57,6 +57,18 @@ function parseReleaseArgs(args = []) {
57
57
  async function handler(args, _flags, projectRoot, opts = {}) {
58
58
  const parsed = parseReleaseArgs(args);
59
59
 
60
+ if (parsed.subcommand === 'regen-audit') {
61
+ // forge release regen-audit — rewrite the D20 kill-list from a live re-scan.
62
+ // The staleness gate (lib/release-readiness d20 check) points here so a
63
+ // Beads-removal PR that shifts the census is a one-command fix, not a
64
+ // hand-edit that red-fails CI until it matches byte-for-byte.
65
+ const { path: artifact } = writeAuditArtifact(projectRoot);
66
+ return {
67
+ success: true,
68
+ output: `Regenerated ${artifact}. Commit it to clear the d20 staleness gate.\n`,
69
+ };
70
+ }
71
+
60
72
  if (parsed.subcommand !== 'check') {
61
73
  // forge release <id> — release a claimed issue via the shared issue dispatch.
62
74
  return runReleaseIssue(args, projectRoot, opts);
@@ -3,62 +3,122 @@
3
3
  const memoryRouter = require('../memory/router');
4
4
  const { stripGlobalFlags } = require('../global-flags');
5
5
 
6
- const usage = 'Usage: forge remember <note> [--tag <label>]... [--json]';
6
+ const usage =
7
+ 'Usage: forge remember <note> [--kind <type>] [--session-summary] [--tag <label>]... ' +
8
+ '[--what <text>] [--why <text>] [--where <text>] [--learned <text>] [--json]';
9
+
10
+ // Structured note fields (kernel issue 8cc1db4d). Each is optional and, when present, is
11
+ // folded into the stored note body as a labeled line so it stays FTS-searchable. Order is
12
+ // fixed for a stable, readable render.
13
+ const STRUCTURED_FIELDS = [
14
+ ['--what', 'What'],
15
+ ['--why', 'Why'],
16
+ ['--where', 'Where'],
17
+ ['--learned', 'Learned'],
18
+ ];
19
+
20
+ // A note's type is stored as a reserved `type:<value>` tag — cheap, additive, and filterable
21
+ // by recall/search WITHOUT a store-schema change (a missing type is fine). The CLI flag is
22
+ // `--kind` (NOT `--type`): `--type` is a reserved GLOBAL flag that bin/forge.js hard-validates
23
+ // against workflow classifications (critical|standard|…), so it can never carry a note type.
24
+ const TYPE_TAG_PREFIX = 'type:';
7
25
 
8
26
  /**
9
- * Split positional note words from `--tag <label>` pairs and the `--json` flag.
10
- * Global flags (e.g. `-p <dir>`) are stripped first so they never leak into
11
- * the stored note content (kernel issue c1e090ff).
27
+ * Split positional note words from `--kind`, `--tag <label>`, the structured field flags,
28
+ * and `--json`. Global flags (e.g. `-p <dir>`) are stripped first so they never leak into the
29
+ * stored note content (kernel issue c1e090ff); `--kind` is NOT a global flag, so it survives
30
+ * the strip and is parsed here.
12
31
  *
13
32
  * @param {string[]} rawArgs - Raw command arguments.
14
- * @returns {{ note: string, tags: string[], json: boolean }}
33
+ * @returns {{ note: string, tags: string[], type: (string|undefined), fields: object, json: boolean }}
15
34
  */
16
35
  function parseArgs(rawArgs) {
17
36
  const args = stripGlobalFlags(rawArgs);
37
+ const fieldFlags = new Map(STRUCTURED_FIELDS.map(([flag, label]) => [flag, label]));
18
38
  const words = [];
19
39
  const tags = [];
40
+ const fields = {};
41
+ let type;
20
42
  let json = false;
21
43
 
44
+ // Consume `--flag value` / `--flag=value`, guarding against swallowing the next flag.
45
+ const takeValue = (index) => {
46
+ const arg = args[index];
47
+ const eq = arg.indexOf('=');
48
+ if (eq >= 0) return { value: arg.slice(eq + 1), consumed: 0 };
49
+ const next = args[index + 1];
50
+ if (next && !next.startsWith('--')) return { value: next, consumed: 1 };
51
+ return { value: undefined, consumed: 0 };
52
+ };
53
+
22
54
  for (let index = 0; index < args.length; index += 1) {
23
55
  const arg = args[index];
56
+ const bare = arg.startsWith('--') ? arg.split('=', 1)[0] : arg;
24
57
  if (arg === '--json') {
25
58
  json = true;
26
- } else if (arg === '--tag') {
27
- const value = args[index + 1];
28
- if (value && !value.startsWith('--')) {
29
- tags.push(value);
30
- index += 1;
31
- }
32
- } else if (arg.startsWith('--tag=')) {
33
- tags.push(arg.slice('--tag='.length));
59
+ } else if (arg === '--session-summary') {
60
+ // Memorable one-flag alias for `--kind session-summary` — the explicit capture-on-exit
61
+ // path an agent calls (via `forge memory add --session-summary` / `forge remember`) to
62
+ // persist a structured session summary with the same --what/--why/--learned fields.
63
+ type = 'session-summary';
64
+ } else if (bare === '--kind') {
65
+ const { value, consumed } = takeValue(index);
66
+ if (value) type = value.trim();
67
+ index += consumed;
68
+ } else if (bare === '--tag') {
69
+ const { value, consumed } = takeValue(index);
70
+ if (value) tags.push(value);
71
+ index += consumed;
72
+ } else if (fieldFlags.has(bare)) {
73
+ const { value, consumed } = takeValue(index);
74
+ if (value) fields[fieldFlags.get(bare)] = value.trim();
75
+ index += consumed;
34
76
  } else {
35
77
  words.push(arg);
36
78
  }
37
79
  }
38
80
 
39
- return { note: words.join(' ').trim(), tags, json };
81
+ return { note: words.join(' ').trim(), tags, type, fields, json };
82
+ }
83
+
84
+ /**
85
+ * Compose the stored note body from the positional note plus any structured fields. Fields
86
+ * are appended as labeled lines under the note so recall renders them readably and they stay
87
+ * searchable. Returns the empty string only when nothing at all was provided.
88
+ */
89
+ function composeBody(note, fields) {
90
+ const lines = STRUCTURED_FIELDS
91
+ .map(([, label]) => (fields[label] ? `${label}: ${fields[label]}` : null))
92
+ .filter(Boolean);
93
+ return [note, ...lines].filter(Boolean).join('\n');
40
94
  }
41
95
 
42
96
  async function handler(args, _flags, projectRoot) {
43
- const { note, tags, json } = parseArgs(args);
97
+ const { note, tags, type, fields, json } = parseArgs(args);
98
+ const body = composeBody(note, fields);
44
99
 
45
- if (!note) {
100
+ if (!body) {
46
101
  return {
47
102
  success: false,
48
103
  error: `No note provided.\n${usage}`,
49
104
  };
50
105
  }
51
106
 
52
- const entry = memoryRouter.append(projectRoot, note, { tags });
107
+ // The type rides as a reserved tag so it is stored and filterable without a schema change.
108
+ const allTags = type ? [...tags, `${TYPE_TAG_PREFIX}${type}`] : tags;
109
+ const entry = memoryRouter.append(projectRoot, body, { tags: allTags });
53
110
 
54
111
  if (json) {
55
- return { success: true, output: `${JSON.stringify(entry, null, 2)}\n` };
112
+ const payload = type ? { ...entry, type } : entry;
113
+ return { success: true, output: `${JSON.stringify(payload, null, 2)}\n` };
56
114
  }
57
115
 
58
- const tagSuffix = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : '';
116
+ const userTags = entry.tags.filter(tag => !tag.startsWith(TYPE_TAG_PREFIX));
117
+ const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
118
+ const typePrefix = type ? `(${type}) ` : '';
59
119
  return {
60
120
  success: true,
61
- output: `Remembered: ${entry.note}${tagSuffix}`,
121
+ output: `Remembered: ${typePrefix}${entry.note}${tagSuffix}`,
62
122
  };
63
123
  }
64
124
 
@@ -67,7 +127,13 @@ module.exports = {
67
127
  description: 'Persist a project-memory note to the kernel-backed memory store',
68
128
  usage,
69
129
  flags: {
130
+ '--kind': 'Type the note (decision|bugfix|gotcha|...) — filterable by recall (--type is reserved)',
131
+ '--session-summary': 'Alias for --kind session-summary — capture a session summary on exit',
70
132
  '--tag': 'Attach a search label (repeatable)',
133
+ '--what': 'Structured field: what happened/changed',
134
+ '--why': 'Structured field: why',
135
+ '--where': 'Structured field: where (file/area)',
136
+ '--learned': 'Structured field: what was learned',
71
137
  '--json': 'Emit machine-readable JSON output',
72
138
  },
73
139
  handler,
@@ -240,7 +240,7 @@ async function routeMutation(verb, args, projectRoot, deps = {}) {
240
240
  function defaultGenerate(projectRoot, dashboardDir) {
241
241
  return new Promise((resolve, reject) => {
242
242
  const script = path.join(dashboardDir, 'generate-snapshot.mjs');
243
- const child = spawn(process.execPath, [script], { cwd: projectRoot, stdio: 'ignore' });
243
+ const child = spawn(process.execPath, [script], { cwd: projectRoot, stdio: 'ignore', windowsHide: true });
244
244
  const timer = setTimeout(() => { child.kill(); reject(new Error('snapshot generation timed out')); }, GEN_TIMEOUT_MS);
245
245
  child.on('error', (err) => { clearTimeout(timer); reject(err); });
246
246
  child.on('exit', (code) => {
@@ -441,7 +441,10 @@ function browserOpener() {
441
441
  function openBrowser(url) {
442
442
  try {
443
443
  const { file, args } = browserOpener();
444
- const child = spawn(file, [...args, url], { detached: true, stdio: 'ignore' });
444
+ // windowsHide keeps this detached opener from flashing a console window on
445
+ // Windows (Node defaults windowsHide to false); background/detached spawns
446
+ // must stay silent (issue 931e7924).
447
+ const child = spawn(file, [...args, url], { detached: true, stdio: 'ignore', windowsHide: true });
445
448
  // spawn() failures (e.g. the opener binary is missing) surface ASYNChronously
446
449
  // as an 'error' event, NOT via the try/catch — an unhandled one would crash
447
450
  // `forge serve`. Opening a browser is best-effort; swallow it (URL is printed).