@sabaiway/agent-workflow-memory 4.4.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ All notable changes to the memory substrate. Versions are this **package's** npm
4
4
  they are distinct from the **deployment-lineage** stamp written into a project's
5
5
  `docs/ai/.memory-version` (which tracks the shared `agent-workflow` lineage, head `3.0.0`).
6
6
 
7
+ ## 4.5.1 — the deployed scripts decide direct-run by real path, so a symlinked entry point stops silently doing nothing (AD-102; ships with kit 5.11.1)
8
+
9
+ **A script invoked through a symlink ran nothing and exited 0.** The guard that decides "was I run
10
+ directly, or imported?" compared `import.meta.url` against `process.argv[1]` — a comparison that is
11
+ false whenever the entry point reached the script through a symlink. The script then took the
12
+ imported-as-a-module path, did no work, and reported success. The fix has been sitting in-repo,
13
+ unpublished, since the delegation series' own measurement came back FAIL and the fix was correctly
14
+ held back from a release it should not have justified (AD-101). It ships here as the plain bug fix it
15
+ is.
16
+
17
+ - **The six standalone scripts this package deploys inline a fail-closed realpath guard.** A deployed
18
+ script cannot reach the kit's shared direct-run leaf, so one lexical line becomes an 11-line IIFE
19
+ that resolves both sides through `realpath` and refuses to guess when it cannot: `references/scripts/`
20
+ `archive-changelog.mjs` · `archive-decisions.mjs` · `archive-issues.mjs` · `check-docs-size.mjs` ·
21
+ `migrate-gates.mjs`, plus `scripts/stamp-takeover.mjs`.
22
+ - **No behaviour changes for a script invoked by its real path** — which is why this is a PATCH. What
23
+ changes is that invoking one through a symlink now does what you asked instead of nothing.
24
+
25
+ Recorded size effect (reason: a standalone deployed script cannot reach the kit's shared direct-run
26
+ leaf, so it inlines the realpath guard — one lexical line becomes an 11-line fail-closed IIFE):
27
+
28
+ ```text
29
+ agent-workflow-memory/references/scripts/archive-changelog.mjs: lines 546 -> 557 (raise)
30
+ agent-workflow-memory/references/scripts/archive-decisions.mjs: lines 1199 -> 1210 (raise)
31
+ agent-workflow-memory/references/scripts/archive-issues.mjs: lines 415 -> 426 (raise)
32
+ agent-workflow-memory/references/scripts/check-docs-size.mjs: lines 580 -> 591 (raise)
33
+ agent-workflow-memory/references/scripts/migrate-gates.mjs: lines 722 -> 733 (raise)
34
+ agent-workflow-memory: aggregate lines 10907 -> 10962 -> 10974 (raise)
35
+ ```
36
+
37
+ ## 4.5.0 — the closing state block gets a canon rule, and its slot labels are declared English (AD-098; ships with kit 5.10.0)
38
+
39
+ **Three slots that answer three different questions, or one restatement written three times.** The
40
+ deployed rules template already asked for a closing state block; what it never said was what belongs
41
+ in each slot — so *now* drifted into a report of finished work, and the block collapsed into a
42
+ summary the reader had already read.
43
+
44
+ - **`agent_rules.md` gains the rule, in §2.5.** *Now* is the state at this instant — what is running
45
+ or what the work is stopped on, never a report of what was completed (that belongs in the message
46
+ body, above the block). *From you* is the real unblocker, named; a turn that is ending always has
47
+ one. *Next* is what follows.
48
+ - **The slot LABELS stay English; the values take the dialogue language.** An English label is what
49
+ lets a state-block checker find the block and its slots at all; everything written into a slot is
50
+ in the project's own language, and the checker's English phrase sets do not judge those values.
51
+ This is the substrate half of the same decision the kit's guard ships.
52
+ - **One test point renamed to describe the check it actually runs** — the `--write-index` refusal is
53
+ a pre-write symlink refusal on the index path, which is what the body has pinned since 4.4.0.
54
+
7
55
  ## 4.4.0 — the deploy finishes by writing the navigator its entry point declares (AD-096; ships with kit 5.9.0)
8
56
 
9
57
  **The substrate deployed an `AGENTS.md` that calls `docs/ai/index.md` always-loaded, and no step
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-memory
3
3
  description: Deploy or upgrade a portable AI-agent memory substrate in any project — an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) and a structured `docs/ai/` context store with cap/archive/index enforcement. Use when the user wants to bootstrap `docs/ai/`, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-memory` / `/agent-workflow-memory upgrade`. Triggers on "set up the memory system", "deploy the AI memory here", "bootstrap docs/ai", "upgrade the memory substrate". This is the substrate only — the workflow methodology (plan→execute→review, queue, Cleanup) is owned elsewhere and injected into AGENTS.md by the family composition root.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '4.4.0'
6
+ version: '4.5.1'
7
7
  ---
8
8
 
9
9
  # agent-workflow-memory
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-memory",
5
5
  "kind": "memory-substrate",
6
- "version": "4.4.0",
6
+ "version": "4.5.1",
7
7
  "provides": ["context"],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-memory",
3
- "version": "4.4.0",
3
+ "version": "4.5.1",
4
4
  "description": "Portable, cross-agent memory substrate for AI coding agents — an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement, deployable standalone or as part of the agent-workflow family. The memory layer of the agent-workflow family.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -32,9 +32,9 @@
32
32
  // --warm-days=N (default 30)
33
33
  // --today=YYYY-MM-DD (default today UTC) — useful for tests / reproducible runs
34
34
 
35
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs';
35
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, realpathSync } from 'node:fs';
36
36
  import { dirname, resolve, basename } from 'node:path';
37
- import { fileURLToPath, pathToFileURL } from 'node:url';
37
+ import { fileURLToPath } from 'node:url';
38
38
  import { tokenizeMarkdown, findParagraphBreak, fail } from './markdown-blocks.mjs';
39
39
 
40
40
  const __filename = fileURLToPath(import.meta.url);
@@ -542,5 +542,16 @@ export const runCli = (argv, deps = {}) => {
542
542
  }
543
543
  };
544
544
 
545
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
545
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
546
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
547
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
548
+ const isDirectRun = (() => {
549
+ const invoked = process.argv[1];
550
+ if (!invoked) return false;
551
+ try {
552
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
553
+ } catch {
554
+ return false;
555
+ }
556
+ })();
546
557
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -75,9 +75,9 @@
75
75
  //
76
76
  // Dependency-free, Node >= 22. Deployed into a consumer's scripts/ like its siblings.
77
77
 
78
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
78
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, realpathSync } from 'node:fs';
79
79
  import { dirname, resolve, join, posix } from 'node:path';
80
- import { fileURLToPath, pathToFileURL } from 'node:url';
80
+ import { fileURLToPath } from 'node:url';
81
81
  import { spawnSync } from 'node:child_process';
82
82
  import { createHash } from 'node:crypto';
83
83
  import { tmpdir } from 'node:os';
@@ -1195,5 +1195,16 @@ export const runCli = (argv, deps = {}) => {
1195
1195
  }
1196
1196
  };
1197
1197
 
1198
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1198
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
1199
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
1200
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
1201
+ const isDirectRun = (() => {
1202
+ const invoked = process.argv[1];
1203
+ if (!invoked) return false;
1204
+ try {
1205
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
1206
+ } catch {
1207
+ return false;
1208
+ }
1209
+ })();
1199
1210
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -31,9 +31,9 @@
31
31
  // --cutoff-days=N (default 14)
32
32
  // --today=YYYY-MM-DD (default UTC today)
33
33
 
34
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
34
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
35
35
  import { dirname, resolve, basename } from 'node:path';
36
- import { fileURLToPath, pathToFileURL } from 'node:url';
36
+ import { fileURLToPath } from 'node:url';
37
37
  import { tokenizeMarkdown, fail } from './markdown-blocks.mjs';
38
38
 
39
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -411,5 +411,16 @@ export const runCli = (argv, deps = {}) => {
411
411
  }
412
412
  };
413
413
 
414
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
414
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
415
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
416
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
417
+ const isDirectRun = (() => {
418
+ const invoked = process.argv[1];
419
+ if (!invoked) return false;
420
+ try {
421
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
422
+ } catch {
423
+ return false;
424
+ }
425
+ })();
415
426
  if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
@@ -1,6 +1,6 @@
1
1
  // check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
2
2
  // in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
3
- // colocated file): the unknown-argument refusal and the written-empty-index guard.
3
+ // colocated file): the unknown-argument refusal and the pre-write symlink refusal on the index path.
4
4
  import { describe, it } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
@@ -20,7 +20,7 @@ describe('check-docs-size runCli — refusal branches', () => {
20
20
  assert.match(stderr, /Unknown argument: --bogus/);
21
21
  });
22
22
 
23
- it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
23
+ it('--write-index refuses a symlinked index path BEFORE writing, naming the path', async () => {
24
24
  const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
25
25
  try {
26
26
  mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
@@ -25,9 +25,9 @@
25
25
  // --quiet print only failures (and final summary)
26
26
 
27
27
  import { readFile, writeFile, readdir, stat, rename, rm } from 'node:fs/promises';
28
- import { existsSync, lstatSync } from 'node:fs';
28
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
29
29
  import { dirname, resolve, relative, join, basename, sep } from 'node:path';
30
- import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { fileURLToPath } from 'node:url';
31
31
  import { randomBytes } from 'node:crypto';
32
32
 
33
33
  const __filename = fileURLToPath(import.meta.url);
@@ -571,7 +571,18 @@ export const runCli = async (argv, deps = {}) => {
571
571
  return result(errorCount > 0 && !flags.report ? 1 : 0);
572
572
  };
573
573
 
574
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
574
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
575
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
576
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
577
+ const isDirectRun = (() => {
578
+ const invoked = process.argv[1];
579
+ if (!invoked) return false;
580
+ try {
581
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
582
+ } catch {
583
+ return false;
584
+ }
585
+ })();
575
586
  if (isDirectRun) {
576
587
  const { code, stdout, stderr } = await runCli(process.argv.slice(2));
577
588
  if (stdout) process.stdout.write(stdout);
@@ -28,7 +28,7 @@
28
28
 
29
29
  import { existsSync, lstatSync, readFileSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
30
30
  import { join, resolve, isAbsolute } from 'node:path';
31
- import { pathToFileURL, fileURLToPath } from 'node:url';
31
+ import { fileURLToPath } from 'node:url';
32
32
  import { randomBytes } from 'node:crypto';
33
33
  import { spawnSync } from 'node:child_process';
34
34
 
@@ -718,5 +718,16 @@ export const main = (argv = process.argv.slice(2), io = {}) => {
718
718
  }
719
719
  };
720
720
 
721
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
721
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
722
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
723
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
724
+ const isDirectRun = (() => {
725
+ const invoked = process.argv[1];
726
+ if (!invoked) return false;
727
+ try {
728
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
729
+ } catch {
730
+ return false;
731
+ }
732
+ })();
722
733
  if (isDirectRun) process.exitCode = main();
@@ -73,6 +73,7 @@ Apply this as part of §2 before any user-facing summary:
73
73
  - **No condescension, no filler.** Own a miss plainly and fix it in the same message.
74
74
  - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
75
75
  - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.
76
+ - **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.
76
77
 
77
78
  ### 2.6. Planning, review & process-fidelity invariants
78
79
  Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
@@ -15,10 +15,11 @@
15
15
  // The Markdown twin `migrations/legacy-stamp-takeover.md` documents the same table as the
16
16
  // no-Node manual fallback. Dependency-free, Node >= 22.
17
17
 
18
+ import { realpathSync } from 'node:fs';
18
19
  import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
19
20
  import { dirname, basename, join, resolve } from 'node:path';
20
21
  import { randomBytes } from 'node:crypto';
21
- import { pathToFileURL } from 'node:url';
22
+ import { fileURLToPath } from 'node:url';
22
23
 
23
24
  // The shared agent-workflow deployment-lineage head. Bumped only when a project-migration
24
25
  // changes the deployed docs/ai structure — NOT on a packaging-only release.
@@ -177,5 +178,16 @@ const main = async (argv) => {
177
178
  if (decision.status === 'stop') process.exit(1);
178
179
  };
179
180
 
180
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
181
+ // Run main() only when executed directly, never on import. Compare by REAL path: an entry point
182
+ // reached through a symlink resolves to its target, so a raw string compare reads the two as
183
+ // different and the CLI never runs. realpathSync collapses the link so both sides match.
184
+ const isDirectRun = (() => {
185
+ const invoked = process.argv[1];
186
+ if (!invoked) return false;
187
+ try {
188
+ return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
189
+ } catch {
190
+ return false;
191
+ }
192
+ })();
181
193
  if (isDirectRun) await main(process.argv.slice(2));