create-agentic-workspace 0.17.0 → 0.17.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/package.json +2 -2
- package/permission-floor.json +1 -1
- package/src/amendmentsBackfill.mjs +25 -6
- package/src/update.mjs +4 -2
- package/src/upgradeReport.mjs +22 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-agentic-workspace",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "The pre-session bootstrap wizard for an Agentic Foundry workspace: declares (never grants) the permission floor, absorbs foundry-bootstrap.sh's out-of-session identity wiring, and scaffolds a seven-file schema-valid workspace. Zero dependencies, no lifecycle scripts, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"marketplace_name": "agentic-foundry",
|
|
35
35
|
"marketplace_repo": "lukasrepublic/agentic-foundry",
|
|
36
36
|
"plugin_name": "foundry",
|
|
37
|
-
"plugin_version": "1.17.
|
|
37
|
+
"plugin_version": "1.17.1",
|
|
38
38
|
"pins_researched": "2026-08-02"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/permission-floor.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"plugin_root_glob": "~/.claude/plugins/cache/*/foundry/*",
|
|
4
|
-
"generated_for_plugin_version": "1.17.
|
|
4
|
+
"generated_for_plugin_version": "1.17.1",
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
7
|
"rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-acceptance-contract-validate.py:*)",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// never written), atomic writes, and a plan/apply split so `--dry-run` prints the same row.
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
+
import { confinedJoin } from './util.mjs';
|
|
17
18
|
|
|
18
19
|
export const NORMATIVE_CLOSE = '<!-- /normative -->';
|
|
19
20
|
export const AMENDMENTS_HEADING = '## Amendments';
|
|
@@ -22,7 +23,10 @@ export const AMENDMENTS_HEADING = '## Amendments';
|
|
|
22
23
|
* "...\n\n" gets the heading directly. Header columns are `foundry-amend.py`'s own row shape. */
|
|
23
24
|
export const AMENDMENTS_BLOCK = `${AMENDMENTS_HEADING}\n\n| date | what changed | why reality required it | auth_seq |\n|---|---|---|---|\n`;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
// ER #223 (v1.17.1): EVERY `*.md` under specs/, not only `feat-*.md` — foundry-amend.py has no
|
|
27
|
+
// filename rule (it classifies by the normative region), and an adopter's delivery atoms are named
|
|
28
|
+
// `spec-*.md`. A README or template without a normative region lands in `skipped`, never written.
|
|
29
|
+
const SPEC_BASENAME_RE = /\.md$/;
|
|
26
30
|
const CODE_FENCE_RE = /```[\s\S]*?```/g;
|
|
27
31
|
|
|
28
32
|
/** The verdict `foundry-amend.py`'s amendments_section_ok gives: `present` when a `## Amendments`
|
|
@@ -42,15 +46,19 @@ export function classifySpec(text) {
|
|
|
42
46
|
return 'absent';
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/** Every regular
|
|
49
|
+
/** Every regular `*.md` under `<root>/specs` (any basename — ER #223), depth-first, with symlinked FILES reported
|
|
46
50
|
* separately (never followed, never written — AC-AMB-2) and symlinked DIRECTORIES not descended
|
|
47
51
|
* (the same confinement instinct as the scaffold's confinedJoin: nothing outside the workspace
|
|
48
52
|
* root is ever touched). Absent `specs/` yields an empty walk, not an error. */
|
|
49
53
|
export function walkSpecs(physicalRoot) {
|
|
50
|
-
const specsRoot = path.join(physicalRoot, 'specs');
|
|
51
54
|
const files = [];
|
|
52
55
|
const symlinks = [];
|
|
53
|
-
|
|
56
|
+
// v1.17.1 security review Risk 1: the ROOT is confined like every entry under it — a `specs`
|
|
57
|
+
// that is itself a symlink (or resolves outside the workspace) is never descended, never written.
|
|
58
|
+
const specsRoot = confinedJoin(physicalRoot, 'specs');
|
|
59
|
+
if (!specsRoot) return { files, symlinks };
|
|
60
|
+
const rootStat = fs.lstatSync(specsRoot, { throwIfNoEntry: false });
|
|
61
|
+
if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory()) return { files, symlinks };
|
|
54
62
|
const stack = [specsRoot];
|
|
55
63
|
while (stack.length > 0) {
|
|
56
64
|
const dir = stack.pop();
|
|
@@ -120,8 +128,19 @@ export function applyAmendmentsBackfill(plan) {
|
|
|
120
128
|
}
|
|
121
129
|
if (classifySpec(text) !== 'absent') continue;
|
|
122
130
|
const tmp = `${abs}.amendments-backfill.tmp`;
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
// v1.17.1 security review Risk 2: `wx` refuses to write through a planted sibling — a symlink
|
|
132
|
+
// or a leftover file at the temp path means this spec is skipped, never written elsewhere.
|
|
133
|
+
try {
|
|
134
|
+
fs.writeFileSync(tmp, text + appendBytesFor(text), { encoding: 'utf-8', flag: 'wx' });
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
fs.renameSync(tmp, abs);
|
|
140
|
+
} catch {
|
|
141
|
+
fs.rmSync(tmp, { force: true });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
125
144
|
written += 1;
|
|
126
145
|
}
|
|
127
146
|
plan.applied = true;
|
package/src/update.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './floorReconcile.mjs';
|
|
17
17
|
import { reconcileGitignorePlan, applyGitignorePlan, renderGitignoreRow } from './gitignoreReconcile.mjs';
|
|
18
18
|
import { planAmendmentsBackfill, applyAmendmentsBackfill, renderAmendmentsRow } from './amendmentsBackfill.mjs';
|
|
19
|
-
import { buildUpgradeReport, writeUpgradeReport, NEXT_LINE } from './upgradeReport.mjs';
|
|
19
|
+
import { buildUpgradeReport, writeUpgradeReport, installedVersionBefore, NEXT_LINE } from './upgradeReport.mjs';
|
|
20
20
|
import { planStatuslineWiring, applyStatuslineWiring, renderStatuslineRows, statuslineChanged } from './statuslineWiring.mjs';
|
|
21
21
|
import {
|
|
22
22
|
ALLOWED_CLAUDE_SUBCOMMANDS, resolveClaudeOnPath, runClaude,
|
|
@@ -124,6 +124,8 @@ export async function runUpdate(argv, { cwd, configDir, homeDir, pkgDir, output,
|
|
|
124
124
|
|
|
125
125
|
const registry = readInstalledPluginsRegistry(configDir);
|
|
126
126
|
const isInstalled = isInstalledInScopeFactory(registry, pluginKey, cwd);
|
|
127
|
+
// ER #222: the report's `from` is what THIS workspace had installed, read before any mutation.
|
|
128
|
+
const installedBefore = installedVersionBefore(registry, pluginKey, cwd);
|
|
127
129
|
|
|
128
130
|
const migrations = [];
|
|
129
131
|
const indeterminateInstalledness = [];
|
|
@@ -336,7 +338,7 @@ export async function runUpdate(argv, { cwd, configDir, homeDir, pkgDir, output,
|
|
|
336
338
|
// completed run (overwritten — a report, not a managed file; `.foundry/*` is gitignored), and
|
|
337
339
|
// named in the LAST line so the operator's next step is never a guess.
|
|
338
340
|
const report = buildUpgradeReport({
|
|
339
|
-
|
|
341
|
+
installedBefore, afterEntry, toPluginVersion: pins.plugin_version, phases, filePlan, amendmentsPlan,
|
|
340
342
|
});
|
|
341
343
|
const reportPath = writeUpgradeReport(physicalRoot, report);
|
|
342
344
|
print('');
|
package/src/upgradeReport.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { confinedJoin } from './util.mjs';
|
|
14
|
+
import { scopeRecordsFor } from './pluginRefresh.mjs';
|
|
14
15
|
|
|
15
16
|
export const REPORT_REL = '.foundry/upgrade-report.json';
|
|
16
17
|
export const REPORT_SCHEMA_VERSION = 1;
|
|
@@ -24,19 +25,22 @@ export function versionOrNull(v) {
|
|
|
24
25
|
return typeof v === 'string' && v.length <= 64 && VERSION_RE.test(v) ? v : null;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/** Pure: assemble the report object. `
|
|
28
|
-
*
|
|
28
|
+
/** Pure: assemble the report object. `installedBefore` is this workspace's installed version from
|
|
29
|
+
* `installedVersionBefore` (or null); `afterEntry` is the marketplace manifest's plugin entry after
|
|
30
|
+
* the refresh (or null); `filePlan` is the managed-file plan;
|
|
29
31
|
* `amendmentsPlan` is the applied backfill plan (or null); `phases` is what renderSummary got. */
|
|
30
32
|
export function buildUpgradeReport({
|
|
31
|
-
|
|
33
|
+
installedBefore = null, afterEntry, toPluginVersion, phases, filePlan, amendmentsPlan, now = new Date(),
|
|
32
34
|
}) {
|
|
33
35
|
const seedRow = (filePlan || []).find((f) => f.seed);
|
|
34
36
|
return {
|
|
35
37
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
36
38
|
ran_at: now.toISOString(),
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
39
|
+
// ER #222 (v1.17.1): THIS workspace's installed version from installed_plugins.json, captured
|
|
40
|
+
// before any mutation — never the marketplace clone's advertised version, which is per machine
|
|
41
|
+
// and already moved by the time Phase 1 reads it. null (no record, unreadable registry, a
|
|
42
|
+
// first install) makes the skill list only the current version's CHANGELOG section.
|
|
43
|
+
from_plugin_version: versionOrNull(installedBefore),
|
|
40
44
|
to_plugin_version: (afterEntry && versionOrNull(afterEntry.version)) || versionOrNull(toPluginVersion),
|
|
41
45
|
phases: (phases || []).map((p) => ({ name: p.name, verdict: p.verdict, ...(p.reason ? { reason: p.reason } : {}) })),
|
|
42
46
|
amendments: amendmentsPlan
|
|
@@ -47,6 +51,18 @@ export function buildUpgradeReport({
|
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/** The version THIS workspace had installed before the run (ER #222): the project-scope record
|
|
55
|
+
* whose `projectPath` is `cwd`, else the user-scope record (no `projectPath`), else null. Reads the
|
|
56
|
+
* registry object `readInstalledPluginsRegistry` returns; `ok: false` is null, never a guess. */
|
|
57
|
+
export function installedVersionBefore(registry, pluginKey, cwd) {
|
|
58
|
+
if (!registry || !registry.ok) return null;
|
|
59
|
+
const records = scopeRecordsFor(registry.doc, pluginKey).filter((r) => r && typeof r === 'object');
|
|
60
|
+
const project = records.find((r) => r.projectPath && path.resolve(String(r.projectPath)) === path.resolve(cwd));
|
|
61
|
+
const user = records.find((r) => !r.projectPath);
|
|
62
|
+
const hit = project || user;
|
|
63
|
+
return hit ? versionOrNull(hit.version) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
/** Write the report under the workspace root, creating `.foundry/` if needed. Overwrites.
|
|
51
67
|
* Confined the way every other Phase-4 writer is (PR #218 security review, Risk 4): the target is
|
|
52
68
|
* joined through `confinedJoin`, a `.foundry` that is a symlink or a leaf that is not a regular
|