@tacuchi/agent-workflow-cli 21.6.0 → 21.7.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.
Files changed (103) hide show
  1. package/dist/application/artifacts-service.js +10 -0
  2. package/dist/application/artifacts-service.js.map +1 -1
  3. package/dist/application/capability/design-handler.js +532 -22
  4. package/dist/application/capability/design-handler.js.map +1 -1
  5. package/dist/application/capability/dispatcher.js +20 -6
  6. package/dist/application/capability/dispatcher.js.map +1 -1
  7. package/dist/application/capability/durable-effect.js +23 -157
  8. package/dist/application/capability/durable-effect.js.map +1 -1
  9. package/dist/application/checkpoint-service.js +49 -3
  10. package/dist/application/checkpoint-service.js.map +1 -1
  11. package/dist/application/checkpoint-write-service.js +5 -0
  12. package/dist/application/checkpoint-write-service.js.map +1 -1
  13. package/dist/application/design/design-content-gate-service.js +134 -0
  14. package/dist/application/design/design-content-gate-service.js.map +1 -0
  15. package/dist/application/design/design-gate-service.js +18 -0
  16. package/dist/application/design/design-gate-service.js.map +1 -1
  17. package/dist/application/design/design-index-service.js +2 -0
  18. package/dist/application/design/design-index-service.js.map +1 -1
  19. package/dist/application/design/design-publish-service.js +91 -19
  20. package/dist/application/design/design-publish-service.js.map +1 -1
  21. package/dist/application/design/design-resolver-service.js +18 -1
  22. package/dist/application/design/design-resolver-service.js.map +1 -1
  23. package/dist/application/design/design-simple-service.js +217 -0
  24. package/dist/application/design/design-simple-service.js.map +1 -0
  25. package/dist/application/flow/advance.js +96 -11
  26. package/dist/application/flow/advance.js.map +1 -1
  27. package/dist/application/flow/flow-service.js +12 -1
  28. package/dist/application/flow/flow-service.js.map +1 -1
  29. package/dist/application/flow/internal-actions.js +221 -0
  30. package/dist/application/flow/internal-actions.js.map +1 -0
  31. package/dist/application/flow/internal-drive.js +293 -0
  32. package/dist/application/flow/internal-drive.js.map +1 -0
  33. package/dist/application/flow/submit.js +242 -84
  34. package/dist/application/flow/submit.js.map +1 -1
  35. package/dist/application/local-proposal.js +171 -0
  36. package/dist/application/local-proposal.js.map +1 -0
  37. package/dist/application/parsers/decisiones.js +13 -0
  38. package/dist/application/parsers/decisiones.js.map +1 -1
  39. package/dist/application/paths-service.js +11 -1
  40. package/dist/application/paths-service.js.map +1 -1
  41. package/dist/application/resume-service.js +22 -26
  42. package/dist/application/resume-service.js.map +1 -1
  43. package/dist/application/semantic-operation/protocol.js +14 -4
  44. package/dist/application/semantic-operation/protocol.js.map +1 -1
  45. package/dist/application/session-artifacts.js +53 -0
  46. package/dist/application/session-artifacts.js.map +1 -1
  47. package/dist/application/session-close-service.js +5 -0
  48. package/dist/application/session-close-service.js.map +1 -1
  49. package/dist/application/session-narrative.js +294 -0
  50. package/dist/application/session-narrative.js.map +1 -0
  51. package/dist/application/status-service.js +1 -0
  52. package/dist/application/status-service.js.map +1 -1
  53. package/dist/application/workline-index-service.js +2 -0
  54. package/dist/application/workline-index-service.js.map +1 -1
  55. package/dist/cli/commands/capability.js +1 -1
  56. package/dist/cli/commands/capability.js.map +1 -1
  57. package/dist/cli/commands/designs.js +29 -5
  58. package/dist/cli/commands/designs.js.map +1 -1
  59. package/dist/cli/commands/flow.js +14 -0
  60. package/dist/cli/commands/flow.js.map +1 -1
  61. package/dist/cli/commands/session-artifacts.js +55 -2
  62. package/dist/cli/commands/session-artifacts.js.map +1 -1
  63. package/dist/cli/parser.js +1 -0
  64. package/dist/cli/parser.js.map +1 -1
  65. package/dist/domain/design/capability.js +35 -4
  66. package/dist/domain/design/capability.js.map +1 -1
  67. package/dist/domain/design/direct.js.map +1 -1
  68. package/dist/domain/design/expansion.js +137 -0
  69. package/dist/domain/design/expansion.js.map +1 -0
  70. package/dist/domain/design/manifest.js +61 -8
  71. package/dist/domain/design/manifest.js.map +1 -1
  72. package/dist/domain/design/reference.js +57 -10
  73. package/dist/domain/design/reference.js.map +1 -1
  74. package/dist/domain/design/simple.js +164 -0
  75. package/dist/domain/design/simple.js.map +1 -0
  76. package/dist/domain/flow/answer.js +90 -12
  77. package/dist/domain/flow/answer.js.map +1 -1
  78. package/dist/domain/flow/authority.js +266 -60
  79. package/dist/domain/flow/authority.js.map +1 -1
  80. package/dist/domain/flow/authorization.js +36 -15
  81. package/dist/domain/flow/authorization.js.map +1 -1
  82. package/dist/domain/flow/directive.js +10 -6
  83. package/dist/domain/flow/directive.js.map +1 -1
  84. package/dist/domain/flow/execution-result.js +72 -0
  85. package/dist/domain/flow/execution-result.js.map +1 -0
  86. package/dist/domain/flow/run-state.js +165 -13
  87. package/dist/domain/flow/run-state.js.map +1 -1
  88. package/dist/domain/host-verification.js +8 -8
  89. package/dist/domain/host-verification.js.map +1 -1
  90. package/dist/domain/proposal.js +91 -0
  91. package/dist/domain/proposal.js.map +1 -0
  92. package/dist/domain/session/narrative.js +126 -0
  93. package/dist/domain/session/narrative.js.map +1 -0
  94. package/package.json +1 -1
  95. package/skills/w/SKILL.md +1 -1
  96. package/skills/w/loops/CHASSIS.md +1 -1
  97. package/skills/w/loops/plan-new-loop/LOOP.md +5 -6
  98. package/skills/w/loops/plan-refine-loop/LOOP.md +7 -7
  99. package/skills/w/loops/spec-refine-loop/LOOP.md +6 -4
  100. package/skills/w/modules/DESIGN-REFERENCES.md +50 -56
  101. package/skills/w/roles/design/CONTRACT.md +73 -6
  102. package/skills/w/roles/design/ROLE.md +21 -3
  103. package/skills/w/schemas/design/design-manifest.v1.schema.json +4 -0
@@ -14,17 +14,27 @@
14
14
  * `create` cannot invent a package body, so it says what a valid answer must
15
15
  * contain and waits for one — that is a question, not a failure.
16
16
  */
17
+ import { join } from "node:path";
17
18
  import { requireAdapter } from "../../domain/design/adapter.js";
18
19
  import { DESIGN_DESCRIPTOR, DESIGN_OPERATIONS } from "../../domain/design/capability.js";
19
20
  import { attainedMaturity, isIndexable, resolveOutputRoot, } from "../../domain/design/direct.js";
21
+ import { deriveStructuralSignals, judgeExpansion, } from "../../domain/design/expansion.js";
22
+ import { DESIGN_MANIFEST_FILE, DESIGN_MANIFEST_SCHEMA_ID, } from "../../domain/design/manifest.js";
20
23
  import { DESIGN_ADAPTERS } from "../../domain/design/profiles.js";
21
- import { classifySource, reportSources } from "../../domain/design/sources.js";
22
- import { readDesignIndex, resolveDesignPackage } from "../design/design-index-service.js";
24
+ import { SIMPLE_CORE_SECTIONS, SIMPLE_DESIGN_FILE, SIMPLE_SECTIONS, designFolder, designSlug, nextPackageId, } from "../../domain/design/simple.js";
25
+ import { classifySource, reportSources, } from "../../domain/design/sources.js";
26
+ import { baseDigest } from "../../domain/proposal.js";
27
+ import { localDateIso } from "../dates.js";
28
+ import { readDesignIndex, resolveDesignPackage, } from "../design/design-index-service.js";
29
+ import { buildPackageCandidate, } from "../design/design-publish-service.js";
23
30
  import { checkRecordPrecondition } from "../design/design-record-service.js";
31
+ import { buildSimpleProposal, resolveSimpleTarget, } from "../design/design-simple-service.js";
24
32
  import { buildSemanticRequest, parseSemanticResponse } from "../semantic-operation/protocol.js";
25
33
  import { registerCapability } from "./dispatcher.js";
26
34
  /** Artefact ceilings for one authored revision. Generous, and still a ceiling. */
27
35
  const LIMITS = { max_artifacts: 60, max_artifact_bytes: 256_000 };
36
+ /** The two operations that AUTHOR content, and therefore the only ones a route applies to. */
37
+ const AUTHORING_OPERATIONS = ["create", "update"];
28
38
  export const designHandler = {
29
39
  descriptor: DESIGN_DESCRIPTOR,
30
40
  async run(ctx) {
@@ -76,6 +86,7 @@ async function validatePackage(ctx) {
76
86
  },
77
87
  ];
78
88
  const report = reportSources([], `${id}`);
89
+ const simple = found.mode === "simple";
79
90
  const maturity = attainedMaturity(requestedMaturity(ctx), found.ok ? "handoff" : "outline", report);
80
91
  const fields = {
81
92
  package: found.id,
@@ -85,9 +96,13 @@ async function validatePackage(ctx) {
85
96
  path: found.path,
86
97
  root: "workspace",
87
98
  indexable: true,
88
- maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
99
+ maturity: { requested: requestedMaturity(ctx), attained: simple ? null : maturity.attained },
89
100
  sources: [],
90
101
  renditions: [],
102
+ // Judging an existing design reports the route it IS, not one this attempt
103
+ // chose: the signals that expanded it were recorded when it was published,
104
+ // and re-deriving them now from a different invocation would invent a cause.
105
+ route: { mode: simple ? "simple" : "package", signals: [], cause: null },
91
106
  };
92
107
  return {
93
108
  kind: "completed",
@@ -147,12 +162,19 @@ async function authoring(ctx) {
147
162
  },
148
163
  };
149
164
  }
165
+ // Simple by default, and the route is decided BEFORE the contract is published:
166
+ // what a valid answer looks like, and where it may land, are different on the
167
+ // two routes, so asking first and classifying afterwards would publish a
168
+ // contract for a route the run is not on.
169
+ const route = await decideRoute(ctx, sources, root.value);
170
+ if (!route.ok)
171
+ return { kind: "blocked", failure: route.failure };
150
172
  const request = buildSemanticRequest({
151
173
  operation: `${ctx.request.capability}.${ctx.request.operation}`,
152
174
  inputs: ctx.request.inputs.map((i) => ({ name: i.name, value: i.value })),
153
- contract: contractFor(ctx.operation.name),
154
- inventory: { root: root.value.root, mode: root.value.kind },
155
- allowedDestinations: [root.value.root],
175
+ contract: route.value.contract,
176
+ inventory: route.value.inventory,
177
+ allowedDestinations: route.value.destinations,
156
178
  limits: LIMITS,
157
179
  readSet: [],
158
180
  readSetBytes: 0,
@@ -160,7 +182,13 @@ async function authoring(ctx) {
160
182
  if (ctx.verb !== "validate") {
161
183
  return {
162
184
  kind: "needs_input",
163
- gaps: [request.contract, `destinos permitidos: ${request.allowed_destinations.join(", ")}`],
185
+ gaps: [
186
+ request.contract,
187
+ `destinos permitidos: ${request.allowed_destinations.join(", ")}`,
188
+ // Answering means quoting this digest back verbatim; naming it here saves
189
+ // the caller from reimplementing `canonicalJson` to recompute it.
190
+ `input_digest: ${request.input_digest}`,
191
+ ],
164
192
  };
165
193
  }
166
194
  if (ctx.answer === null || ctx.answer.trim().length === 0) {
@@ -175,37 +203,463 @@ async function authoring(ctx) {
175
203
  const parsed = parseSemanticResponse(ctx.answer, request);
176
204
  if (!parsed.ok)
177
205
  return { kind: "blocked", failure: parsed.failure };
178
- const artifacts = (parsed.value.artifacts ?? []).map((a) => ({
179
- path: a.path,
180
- content: a.content,
206
+ const answered = parsed.value.artifacts ?? [];
207
+ return route.value.target === null
208
+ ? packageProposal(ctx, report, route.value, answered)
209
+ : simpleProposal(ctx, report, route.value, answered);
210
+ }
211
+ /**
212
+ * Which route this attempt runs on, and everything that follows from it.
213
+ *
214
+ * The structural half of the vocabulary is derived here from what the invocation
215
+ * really carries; the semantic half is whatever the caller declared, judged
216
+ * against the same closed list. Nothing else can widen the route: an unknown id
217
+ * or a structural one somebody typed comes back as a rejection in the verdict
218
+ * rather than as an expansion nobody can explain.
219
+ */
220
+ async function decideRoute(ctx, sources, root) {
221
+ const index = ctx.workspace === null ? null : await readDesignIndex(ctx.fs, ctx.workspace);
222
+ // By IDENTITY and only when one was named. `find(p => p.id === null)` would
223
+ // match the first package whose manifest does not validate — an entry that has
224
+ // no identity is not the one this invocation continues.
225
+ const named = packageInput(ctx);
226
+ const targeted = index === null || named === null ? null : (index.packages.find((p) => p.id === named) ?? null);
227
+ const verdict = judgeExpansion(declaredExpansionSignals(ctx), deriveStructuralSignals({
228
+ sensitiveSources: ctx.request.policy.sensitive_sources === true,
229
+ externalTransmission: ctx.request.policy.external_transmission === true,
230
+ sources,
231
+ governanceRecords: (targeted?.manifest?.governance.reviews.length ?? 0) +
232
+ (targeted?.manifest?.governance.revocations.length ?? 0),
233
+ publishedRevisions: targeted?.manifest?.baselines.length ?? 0,
181
234
  }));
182
- // The gate verdict the `012` computes is not available until the package is
183
- // on disk, so a proposal can never claim more than `outline` here — which is
184
- // exactly the rule that stops a partial from being promoted.
185
- const maturity = attainedMaturity(requestedMaturity(ctx), "outline", report);
235
+ // The package route is also the only one available outside a workspace or
236
+ // outside `docs/designs/`: a simple design derives its identity from the index,
237
+ // and there is no index to derive it from.
238
+ //
239
+ // `render` and `record` are package operations whatever the signals say —
240
+ // projecting revisions and sealing governance decisions are things a catalog
241
+ // has and one document does not. Routing them by the vocabulary would ask a
242
+ // simple design for a maturity and a rendition it never had.
243
+ if (verdict.mode === "package" ||
244
+ !AUTHORING_OPERATIONS.includes(ctx.operation.name) ||
245
+ index === null ||
246
+ !isIndexable(root)) {
247
+ return packageRoute(ctx, verdict, index, root);
248
+ }
249
+ const resolved = resolveSimpleTarget(index, ctx.operation.name, {
250
+ title: textInput(ctx, "title"),
251
+ packageId: packageInput(ctx),
252
+ });
253
+ if (!resolved.ok) {
254
+ const { code, message, action } = resolved.failure;
255
+ return { ok: false, failure: { code, message, action } };
256
+ }
257
+ return {
258
+ ok: true,
259
+ value: {
260
+ verdict,
261
+ contract: simpleContract(resolved.value),
262
+ inventory: {
263
+ root: root.root,
264
+ mode: "simple",
265
+ package: resolved.value.packageId,
266
+ revision: resolved.value.revision,
267
+ document: `${resolved.value.path}/${SIMPLE_DESIGN_FILE}`,
268
+ },
269
+ // The exact file, not its folder: on the simple route the CLI already knows
270
+ // the one destination, so anything else is not a design it can publish.
271
+ destinations: [`${resolved.value.path}/${SIMPLE_DESIGN_FILE}`],
272
+ target: resolved.value,
273
+ packageTarget: null,
274
+ root,
275
+ },
276
+ };
277
+ }
278
+ /**
279
+ * The package route: sealed when the target can be derived, verbatim when not.
280
+ *
281
+ * The compare-and-swap base is the caller's claim on this route, and a safety
282
+ * check that can be omitted is one nobody performs. It is demanded HERE rather
283
+ * than in the descriptor because the simple route derives it instead.
284
+ */
285
+ function packageRoute(ctx, verdict, index, root) {
286
+ if (ctx.operation.name === "update" && textInput(ctx, "base") === null) {
287
+ return {
288
+ ok: false,
289
+ failure: {
290
+ code: "DESIGN_FIELD_INVALID",
291
+ message: "actualizar un package declara sobre qué revisión se preparó",
292
+ action: "pasá 'base' con la revisión vigente (por ejemplo DES-001@r3), o null si el package no publicó ninguna",
293
+ },
294
+ };
295
+ }
296
+ // KNOWN LIMITATION: `render`/`record` — and a create/update with no index to
297
+ // derive from (outside a workspace or outside `docs/designs/`) — still run
298
+ // the verbatim path: the answer is published as authored, with no derived
299
+ // manifest, baseline or gate. Only create/update over the index seal.
300
+ if (index === null || !AUTHORING_OPERATIONS.includes(ctx.operation.name) || !isIndexable(root)) {
301
+ return {
302
+ ok: true,
303
+ value: {
304
+ verdict,
305
+ contract: contractFor(ctx.operation.name),
306
+ inventory: { root: root.root, mode: root.kind },
307
+ destinations: [root.root],
308
+ target: null,
309
+ packageTarget: null,
310
+ root,
311
+ },
312
+ };
313
+ }
314
+ const resolved = resolvePackageTarget(ctx, index);
315
+ if (!resolved.ok)
316
+ return { ok: false, failure: resolved.failure };
317
+ const target = resolved.value;
318
+ return {
319
+ ok: true,
320
+ value: {
321
+ verdict,
322
+ contract: packageContract(ctx.operation.name, target),
323
+ inventory: {
324
+ root: root.root,
325
+ mode: "package",
326
+ package: target.packageId,
327
+ revision: target.revision,
328
+ path: target.path,
329
+ },
330
+ // The package folder, not the taxonomy root: the destination check is
331
+ // segment-based, so every artifact lands INSIDE this package or nowhere.
332
+ destinations: [target.path],
333
+ target: null,
334
+ packageTarget: target,
335
+ root,
336
+ },
337
+ };
338
+ }
339
+ /**
340
+ * The package route's target, derived BEFORE the contract is published.
341
+ *
342
+ * `create` mints the identity and the folder from the title, over an initial
343
+ * manifest that exists only to give the candidate builder a line to start from.
344
+ * `update` locates the package BY IDENTITY and checks the declared base against
345
+ * the line in force — the same compare-and-swap `publishDesignRevision` runs,
346
+ * moved to the moment the contract is fixed instead of discovered mid-publish.
347
+ */
348
+ function resolvePackageTarget(ctx, index) {
349
+ if (ctx.operation.name === "create") {
350
+ const title = textInput(ctx, "title");
351
+ if (title === null) {
352
+ return {
353
+ ok: false,
354
+ failure: {
355
+ code: "DESIGN_FIELD_INVALID",
356
+ message: "un package nuevo necesita un título",
357
+ action: "pasá 'title' con el nombre humano del diseño: de ahí salen la carpeta y el id",
358
+ },
359
+ };
360
+ }
361
+ const packageId = nextPackageId(index.packages.map((p) => p.id ?? p.declared_id));
362
+ return {
363
+ ok: true,
364
+ value: {
365
+ packageId,
366
+ path: designFolder(index.root, packageId, designSlug(title)),
367
+ revision: 1,
368
+ manifest: initialPackageManifest(packageId, title, localDateIso(new Date())),
369
+ },
370
+ };
371
+ }
372
+ const named = packageInput(ctx);
373
+ if (named === null) {
374
+ return {
375
+ ok: false,
376
+ failure: {
377
+ code: "DESIGN_FIELD_INVALID",
378
+ message: "actualizar un package necesita la identidad del que se continúa",
379
+ action: "pasá 'package' con su id, por ejemplo DES-007",
380
+ },
381
+ };
382
+ }
383
+ const found = index.packages.find((p) => p.id === named) ?? null;
384
+ if (found === null || found.manifest === null) {
385
+ return {
386
+ ok: false,
387
+ failure: {
388
+ code: "DESIGN_PACKAGE_NOT_FOUND",
389
+ message: `no hay ningún package ${named} legible bajo ${index.root}/`,
390
+ action: `revisá 'aw designs' para ver las identidades publicadas bajo ${index.root}/`,
391
+ },
392
+ };
393
+ }
394
+ const manifest = found.manifest;
395
+ const current = manifest.current_baseline;
396
+ const actual = current === null ? null : `${manifest.id}@r${current.revision}`;
397
+ // The declared base is text on the wire: "null" is how a caller states the
398
+ // package never published, which the failure below already advertised. The
399
+ // null case cannot happen — `decideRoute` demands the input first — and the
400
+ // guard keeps that a fact of this function, not of its caller.
401
+ const declaredRaw = textInput(ctx, "base");
402
+ if (declaredRaw === null) {
403
+ return {
404
+ ok: false,
405
+ failure: {
406
+ code: "DESIGN_FIELD_INVALID",
407
+ message: "actualizar un package declara sobre qué revisión se preparó",
408
+ action: "pasá 'base' con la revisión vigente (por ejemplo DES-001@r3), o null si el package no publicó ninguna",
409
+ },
410
+ };
411
+ }
412
+ const declared = declaredRaw === "null" ? null : declaredRaw;
413
+ if (declared !== actual) {
414
+ return {
415
+ ok: false,
416
+ failure: {
417
+ code: "DESIGN_BASE_STALE",
418
+ message: `declaraste base ${declared ?? "ninguna revisión"} y la vigente es ${actual ?? "ninguna"}`,
419
+ action: "releé el package y rehacé la revisión sobre la base nueva: una publicada no se reescribe",
420
+ },
421
+ };
422
+ }
423
+ return {
424
+ ok: true,
425
+ value: {
426
+ packageId: manifest.id,
427
+ path: found.path,
428
+ revision: (current?.revision ?? 0) + 1,
429
+ manifest,
430
+ },
431
+ };
432
+ }
433
+ /**
434
+ * The manifest a `create` starts from: the empty catalog every package begins
435
+ * with, so the candidate builder sees the same shape it sees on an update. It
436
+ * is never written as-is — the first publication writes the DERIVED one.
437
+ */
438
+ function initialPackageManifest(packageId, title, created) {
439
+ return {
440
+ schema: DESIGN_MANIFEST_SCHEMA_ID,
441
+ id: packageId,
442
+ mode: "package",
443
+ title,
444
+ created,
445
+ derived_from: null,
446
+ current_baseline: null,
447
+ baselines: [],
448
+ catalog: { flows: [], screens: [], rules: [], tokens: [], renditions: [], assets: [] },
449
+ currentness: [],
450
+ governance: { reviews: [], revocations: [] },
451
+ relations: { specs: [], plans: [] },
452
+ };
453
+ }
454
+ /** The simple route's durable step: one authored document, everything else derived. */
455
+ async function simpleProposal(ctx, report, route, answered) {
456
+ const target = route.target;
457
+ const documentPath = `${target.path}/${SIMPLE_DESIGN_FILE}`;
458
+ const document = answered.find((a) => a.path === documentPath);
459
+ if (answered.length !== 1 || document === undefined) {
460
+ return {
461
+ kind: "blocked",
462
+ failure: {
463
+ code: "DESIGN_FIELD_INVALID",
464
+ message: `un diseño simple es un solo archivo y llegaron ${answered.length}`,
465
+ action: `respondé exactamente un artefacto en '${documentPath}': el manifest, el id, la revisión y el digest los deriva el CLI`,
466
+ },
467
+ };
468
+ }
469
+ // A target only exists when `decideRoute` read the index, which it only does
470
+ // inside a workspace: the simple route and a null workspace cannot coexist.
471
+ const built = await buildSimpleProposal(ctx.fs, ctx.workspace, {
472
+ target,
473
+ document: document.content,
474
+ published: localDateIso(new Date()),
475
+ });
476
+ if (!built.ok) {
477
+ const first = built.failures[0];
478
+ return {
479
+ kind: "blocked",
480
+ failure: {
481
+ code: first?.code ?? "DESIGN_FIELD_INVALID",
482
+ message: first?.message ?? "el documento no cumple el contrato de un diseño simple",
483
+ action: first?.action ?? "corregí el documento y volvé a responder",
484
+ },
485
+ };
486
+ }
186
487
  const fields = {
187
- package: typeof inputValue(ctx, "package") === "string" ? String(inputValue(ctx, "package")) : null,
188
- baseline: null,
189
- path: root.value.root,
190
- root: root.value.kind,
191
- indexable: isIndexable(root.value),
488
+ package: built.value.packageId,
489
+ baseline: { revision: built.value.revision, digest: built.value.digest },
490
+ path: target.path,
491
+ root: route.root.kind,
492
+ indexable: true,
493
+ // No ladder to climb: see `DesignReceiptFields.maturity`.
494
+ maturity: { requested: requestedMaturity(ctx), attained: null },
495
+ sources: report.sources,
496
+ renditions: [],
497
+ route: routeOf(route.verdict),
498
+ };
499
+ return {
500
+ kind: "durable",
501
+ artifacts: built.value.artifacts.map((a) => ({
502
+ path: a.path,
503
+ content: a.content,
504
+ overwrite: a.overwrite,
505
+ })),
506
+ output: {
507
+ value: {
508
+ design: fields,
509
+ artifacts: built.value.artifacts.map((a) => a.path),
510
+ gaps: [],
511
+ },
512
+ reference: null,
513
+ completeness: "partial",
514
+ },
515
+ base: built.value.base,
516
+ };
517
+ }
518
+ /**
519
+ * The expanded route's durable step.
520
+ *
521
+ * With a derived target (create/update over the index) the CLI owns the seal:
522
+ * the authored artifacts are candidate files, and the manifest, the baseline
523
+ * and `PACKAGE.md` are DERIVED here — the same candidate `publishDesignRevision`
524
+ * publishes, so the gate verdict the `012` computes runs NOW, inside `validate`,
525
+ * and an invalid tree is blocked before the first byte moves.
526
+ *
527
+ * Without one (`render`/`record`, or no index to derive from) the package is
528
+ * published exactly as authored. That verbatim path is the known limitation
529
+ * `decideRoute` documents, not a second contract.
530
+ */
531
+ async function packageProposal(ctx, report, route, answered) {
532
+ const target = route.packageTarget;
533
+ if (target === null) {
534
+ const artifacts = answered.map((a) => ({ path: a.path, content: a.content }));
535
+ const maturity = attainedMaturity(requestedMaturity(ctx), "outline", report);
536
+ const fields = {
537
+ package: packageInput(ctx),
538
+ baseline: null,
539
+ path: route.root.root,
540
+ root: route.root.kind,
541
+ indexable: isIndexable(route.root),
542
+ maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
543
+ sources: report.sources,
544
+ renditions: [],
545
+ route: routeOf(route.verdict),
546
+ };
547
+ return {
548
+ kind: "durable",
549
+ artifacts,
550
+ output: {
551
+ value: { design: fields, artifacts: artifacts.map((a) => a.path), gaps: maturity.gaps },
552
+ reference: null,
553
+ // The durable step has not run yet, so nothing is published. Claiming
554
+ // `complete` here would let a gate accept a proposal as a package.
555
+ completeness: "partial",
556
+ },
557
+ base: null,
558
+ };
559
+ }
560
+ // From workspace-relative to package-relative, which is the vocabulary the
561
+ // candidate builder speaks. The destination check already confined every
562
+ // answer to the package folder, so the prefix always strips.
563
+ const prefix = `${target.path}/`;
564
+ const files = [];
565
+ for (const artifact of answered) {
566
+ const relative = artifact.path.slice(prefix.length);
567
+ if (CLI_DERIVED_FILES.includes(relative) || relative.startsWith("baselines/")) {
568
+ return {
569
+ kind: "blocked",
570
+ failure: {
571
+ code: "DESIGN_FIELD_INVALID",
572
+ message: `'${artifact.path}' no se autora: lo deriva y sella el CLI`,
573
+ action: "quitalo de 'artifacts': el CLI deriva y sella design-manifest.json, baselines/ y PACKAGE.md a partir de los artefactos normativos",
574
+ },
575
+ };
576
+ }
577
+ files.push({ path: relative, content: artifact.content });
578
+ }
579
+ // The target only exists when `decideRoute` read the index, which it only
580
+ // does inside a workspace.
581
+ const candidate = await buildPackageCandidate(ctx.fs, ctx.workspace, {
582
+ manifest: target.manifest,
583
+ packagePath: target.path,
584
+ files,
585
+ published: localDateIso(new Date()),
586
+ });
587
+ if (!candidate.ok) {
588
+ // The gate's own verdict, with its real code and next action: this is where
589
+ // an invalid tree stops being publishable instead of being sealed verbatim.
590
+ const first = candidate.failures[0];
591
+ return {
592
+ kind: "blocked",
593
+ failure: {
594
+ code: first?.code ?? "DESIGN_FIELD_INVALID",
595
+ message: first?.message ?? "el package no cumple el contrato de publicación",
596
+ action: first?.action ?? "corregí los artefactos y volvé a responder",
597
+ },
598
+ };
599
+ }
600
+ // The gate passed over the merged catalog, so the verdict a proposal may
601
+ // claim is what THIS revision's flows and screens claim for themselves — the
602
+ // ones already sealed were judged when they were published.
603
+ const maturity = attainedMaturity(requestedMaturity(ctx), claimedMaturity(candidate.value.manifest, files), report);
604
+ const fields = {
605
+ package: target.packageId,
606
+ baseline: { revision: candidate.value.revision, digest: candidate.value.baseline.digest },
607
+ path: target.path,
608
+ root: route.root.kind,
609
+ indexable: isIndexable(route.root),
192
610
  maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
193
611
  sources: report.sources,
194
612
  renditions: [],
613
+ route: routeOf(route.verdict),
195
614
  };
196
615
  return {
197
616
  kind: "durable",
198
- artifacts,
617
+ artifacts: candidate.value.artifacts,
199
618
  output: {
200
- value: { design: fields, artifacts: artifacts.map((a) => a.path), gaps: maturity.gaps },
619
+ value: {
620
+ design: fields,
621
+ artifacts: candidate.value.artifacts.map((a) => a.path),
622
+ gaps: maturity.gaps,
623
+ },
201
624
  reference: null,
202
625
  // The durable step has not run yet, so nothing is published. Claiming
203
626
  // `complete` here would let a gate accept a proposal as a package.
204
627
  completeness: "partial",
205
628
  },
206
- base: null,
629
+ base: await packageManifestBase(ctx, target),
207
630
  };
208
631
  }
632
+ /** Package-relative file paths the CLI derives and seals — authoring one is rejected. */
633
+ const CLI_DERIVED_FILES = [DESIGN_MANIFEST_FILE, "PACKAGE.md"];
634
+ /**
635
+ * The maturity this revision's own flows and screens claim, as the merged
636
+ * catalog recorded it: `outline` as soon as one of them claims no more, and
637
+ * `handoff` when none says otherwise — the kinds without a maturity ladder
638
+ * have no vote.
639
+ */
640
+ function claimedMaturity(manifest, files) {
641
+ const introduced = new Set(files.map((f) => f.path));
642
+ const claims = [...manifest.catalog.flows, ...manifest.catalog.screens]
643
+ .filter((entry) => introduced.has(entry.path))
644
+ .map((entry) => entry.maturity);
645
+ return claims.includes("outline") ? "outline" : "handoff";
646
+ }
647
+ /**
648
+ * The compare-and-swap base of a package proposal: the manifest as it stood
649
+ * when the candidate was computed. Null when there is nothing on disk to have
650
+ * moved — a create is protected by its destinations not existing.
651
+ */
652
+ async function packageManifestBase(ctx, target) {
653
+ const path = `${target.path}/${DESIGN_MANIFEST_FILE}`;
654
+ const absolute = join(ctx.workspace, path);
655
+ if (!(await ctx.fs.exists(absolute)))
656
+ return null;
657
+ return { path, digest: baseDigest(await ctx.fs.readText(absolute)) };
658
+ }
659
+ /** The verdict as the receipt states it: mode, signals and the one-line cause. */
660
+ function routeOf(verdict) {
661
+ return { mode: verdict.mode, signals: verdict.fired, cause: verdict.cause };
662
+ }
209
663
  /**
210
664
  * What each operation has to be true BEFORE it can even ask for content.
211
665
  *
@@ -279,6 +733,38 @@ function requestedMaturity(ctx) {
279
733
  const value = inputValue(ctx, "maturity");
280
734
  return value === "handoff" || value === "outline" ? value : null;
281
735
  }
736
+ /**
737
+ * The expansion signals the caller put on the table.
738
+ *
739
+ * Accepted as a repeated input or as one comma-separated value, because a
740
+ * command line and a composing flow hand lists over differently — and neither
741
+ * spelling should be the one that silently drops a signal. Whether each id is
742
+ * admissible is `judgeExpansion`'s call, not this reader's.
743
+ */
744
+ function declaredExpansionSignals(ctx) {
745
+ const raw = ctx.request.inputs.filter((i) => i.name === "expansion").map((i) => i.value);
746
+ return raw
747
+ .flatMap((value) => (Array.isArray(value) ? value.map(String) : String(value).split(",")))
748
+ .map((id) => id.trim())
749
+ .filter((id) => id.length > 0);
750
+ }
751
+ function textInput(ctx, name) {
752
+ const value = inputValue(ctx, name);
753
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
754
+ }
755
+ function packageInput(ctx) {
756
+ return textInput(ctx, "package");
757
+ }
758
+ /** What a valid answer is on the simple route: one document, three sections, no metadata. */
759
+ function simpleContract(target) {
760
+ return [
761
+ `Escribí el diseño como UN solo documento Markdown en '${target.path}/${SIMPLE_DESIGN_FILE}'.`,
762
+ `Abrilo con '# <título>' y usá solo estas secciones, en este orden: ${SIMPLE_SECTIONS.map((s) => `## ${s}`).join(", ")}.`,
763
+ `Siempre van ${SIMPLE_CORE_SECTIONS.map((s) => `'## ${s}'`).join(", ")}; las demás solo si dicen algo, y nunca vacías.`,
764
+ "No escribas manifest, id, revisión, digest, madurez ni referencias: todo eso lo deriva el CLI de este documento.",
765
+ "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' y 'artifacts': [{path, content}] con ese único archivo.",
766
+ ].join(" ");
767
+ }
282
768
  function contractFor(operation) {
283
769
  const shared = "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' " +
284
770
  "y 'artifacts': [{path, content}]. Cada 'path' es relativo al workspace y cae dentro de los " +
@@ -291,6 +777,30 @@ function contractFor(operation) {
291
777
  };
292
778
  return `${perOperation[operation] ?? ""} ${shared}`.trim();
293
779
  }
780
+ /**
781
+ * What a valid answer is on the SEALED package route: the normative artifacts,
782
+ * and nothing the CLI derives.
783
+ *
784
+ * The split is the same one the simple route states: the agent authors content,
785
+ * the CLI owns identity, sealing and projections. Naming the derived files in
786
+ * the contract is what keeps a hand-authored manifest or baseline from coming
787
+ * back as an answer — those arrive as a rejection, not as a silent overwrite.
788
+ */
789
+ function packageContract(operation, target) {
790
+ const perOperation = {
791
+ create: "Autorá la PRIMERA revisión del package a partir de las fuentes declaradas.",
792
+ update: "Autorá la revisión SIGUIENTE sobre la base declarada. No reescribas revisiones ya selladas.",
793
+ };
794
+ return [
795
+ perOperation[operation] ?? "",
796
+ `El id asignado es '${target.packageId}' y la carpeta '${target.path}': ambos van en el 'inventory' del request.`,
797
+ `El frontmatter de cada artefacto declara ese id de package (por ejemplo '${target.packageId}/FLW-001').`,
798
+ "NO autores 'design-manifest.json', nada bajo 'baselines/' ni 'PACKAGE.md': el CLI los deriva y sella a partir de tus artefactos, y rechaza la respuesta si los incluye.",
799
+ "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' y 'artifacts': [{path, content}]. Cada 'path' es relativo al workspace y cae dentro de los destinos permitidos. Ningún artefacto inventa un formato: los del UI Design Package v1 son los únicos aceptados.",
800
+ ]
801
+ .join(" ")
802
+ .trim();
803
+ }
294
804
  function inputValue(ctx, name) {
295
805
  return ctx.request.inputs.find((i) => i.name === name)?.value;
296
806
  }