sshlg-skills 1.74.0 → 1.74.2

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
@@ -1,3 +1,24 @@
1
+ ## v1.74.2 (2026-10-22): an unread Codex is not a green step
2
+
3
+ - **B-252 closed:** the 1.74.1 `update` printed "11 of 11 steps, all green" beside "not read:
4
+ codex plugin list failed — spawnSync codex ETIMEDOUT", and Codex stayed at older pins. The
5
+ read took 31.5 s at a load average near 60 against a 30 s limit. A Codex read now has 120 s
6
+ (`SSHLG_CODEX_READ_TIMEOUT_MS` overrides, never below one second), and a Codex that cannot be
7
+ read makes the run PARTIAL instead of green. `test/codex_unread_test.js` (4 cases, red on
8
+ 1.74.1).
9
+
10
+ ## v1.74.1 (2026-10-22): a git worktree's local scope no longer fails the Claude update
11
+
12
+ - **B-251 closed:** the 1.74.0 `update` on the operator machine failed xr-dev (and rolled it
13
+ back) because one of its local scopes was a linked git worktree. Claude Code 2.1.299 runs a
14
+ local-scope `update` from a worktree against its main repository's record and exits 0
15
+ (measured in a scratch HOME: "updated ... for scope local (<repo>)", the worktree's record
16
+ unchanged), so the transaction's readback saw the wrong record move. The planner
17
+ (`lib/claude-pinned.js`) now leaves a worktree's record as recorded beside a recorded main
18
+ repository, disclosed as `left as recorded: <worktree> (alias of <main>)`, and blocks the member
19
+ by name when the main repository has no record. A submodule checkout is unaffected.
20
+ `test/claude_pinned_test.js` 34 to 37 cases.
21
+
1
22
  ## v1.74.0 (2026-10-22): the design-review gate, subscription growth, milestones M1 and M2
2
23
 
3
24
  - **Pins:** task-pipeline 1.101.0 (1.100.0's design-review gate: the `design-tester` agent, a
@@ -875,11 +875,14 @@ function codexPinRef(s, opts) {
875
875
  * counted like every other `update` step, and a failure lands in the closing summary; a
876
876
  * read (`--version`, `plugin list`) is captured and stays quiet.
877
877
  */
878
+ // A read of Codex's plugin list took 31.5 s on a loaded machine (2026-10-22); 30 s timed it
879
+ // out. SSHLG_CODEX_READ_TIMEOUT_MS overrides, never below one second.
880
+ const CODEX_READ_TIMEOUT_MS = Math.max(1000, Number(process.env.SSHLG_CODEX_READ_TIMEOUT_MS) || 120000);
878
881
  function codexRunner(cmd, args, opts) {
879
882
  if (!opts || !opts.read || !(args.length === 1 && args[0] === '--version'
880
883
  || args[0] === 'plugin' && args[1] === 'list' && (args.length === 2 || args.length === 3 && args[2] === '--json'))) throw new Error('Codex native mutation is deferred');
881
884
  const r = spawnSync(cmd, args, { encoding: 'utf8', shell: process.platform === 'win32',
882
- stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000, maxBuffer: 32 * 1024 * 1024 });
885
+ stdio: ['ignore', 'pipe', 'pipe'], timeout: CODEX_READ_TIMEOUT_MS, maxBuffer: 32 * 1024 * 1024 });
883
886
  return { status: r.status, signal: r.signal, stdout: r.stdout || '', stderr: r.stderr || '', error: r.error };
884
887
  }
885
888
 
@@ -921,7 +924,8 @@ function refreshCodexPlugins(f) {
921
924
  }
922
925
  if (out.status === 'unread') {
923
926
  log(` not read: codex plugin list failed — ${out.why}. Nothing in Codex was touched.`);
924
- return true;
927
+ // Not looked is not green: the summary reads PARTIAL and names this section.
928
+ return false;
925
929
  }
926
930
  for (const it of out.items) {
927
931
  if (it.action === 'skip') log(` ${it.pluginId}: left alone — ${it.why}`);
@@ -4090,4 +4094,4 @@ if (require.main === module) {
4090
4094
  process.exit(main(process.argv));
4091
4095
  }
4092
4096
 
4093
- module.exports = { cliSpec, pinnedArgv, resolvePayloads, resolveProviders };
4097
+ module.exports = { cliSpec, pinnedArgv, resolvePayloads, resolveProviders, refreshCodexPlugins, CODEX_READ_TIMEOUT_MS };
@@ -55,6 +55,14 @@ function configured(manifest) {
55
55
  // Claude spells one payload directory differently per record (/var and /private/var on
56
56
  // macOS, measured on 2.1.299), so installs are compared by their physical path.
57
57
  const physical=p=>{try{return fs.realpathSync(p);}catch(_){return p;}};
58
+ // The main working tree of a linked git worktree (`.git` is a file naming
59
+ // `<main>/.git/worktrees/<name>`), or null. A submodule's `.git/modules/` is not one.
60
+ function worktreeMain(dir) {
61
+ let text;try{text=fs.readFileSync(path.join(dir,'.git'),'utf8');}catch(_){return null;}
62
+ const m=/^gitdir:\s*(.+?)\s*$/m.exec(text);if(!m) return null;
63
+ const gitdir=path.resolve(dir,m[1]),at=gitdir.lastIndexOf(path.sep+'.git'+path.sep+'worktrees'+path.sep);
64
+ return at<0?null:physical(gitdir.slice(0,at));
65
+ }
58
66
  function prepare(members,inventory,opts) {
59
67
  const requested={profile:path.resolve(opts.profileRoot),home:path.resolve(opts.home)};
60
68
  const rows=Source.validate(inventory,members),profile=Bindings.profilePath(requested.profile),home=Bindings.profilePath(requested.home);
@@ -109,6 +117,14 @@ function prepare(members,inventory,opts) {
109
117
  for(const r of existing) {
110
118
  if(r.scope==='user') {targets.push({scope:'user',projectPath:null,record:r});continue;}
111
119
  let real=null;try{real=fs.realpathSync(r.projectPath);}catch(_){}
120
+ // A linked git worktree: Claude runs a local-scope update from it against its main
121
+ // repository's record (measured on 2.1.299, 2026-10-22), so the worktree's own
122
+ // record is left as recorded beside a recorded main, and blocks without one.
123
+ const main=real===r.projectPath?worktreeMain(real):null;
124
+ if(main) {
125
+ if(locals.some(o=>o!==r && o.projectPath===main)) {aliases.push({projectPath:r.projectPath,real:main,record:r});continue;}
126
+ unaddressable=r.projectPath+' is a git worktree of '+main+', which has no record';break;
127
+ }
112
128
  if(real===r.projectPath && fs.statSync(real).isDirectory()) targets.push({scope:'local',projectPath:r.projectPath,record:r});
113
129
  // A path that resolves to ANOTHER record's project is that project under an
114
130
  // old name: Claude never matches it, so it is left as recorded and read back.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sshlg-skills",
3
- "version": "1.74.0",
3
+ "version": "1.74.2",
4
4
  "skillsCli": "skills@1.7.2",
5
5
  "description": "The ssheleg harness skill layer: routing, delivery gates, specialist skills and durable handoffs for coding agents.",
6
6
  "bin": {