@hasna/loops 0.3.50 → 0.3.51
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/README.md +5 -3
- package/dist/cli/index.js +63 -18
- package/dist/daemon/index.js +1 -1
- package/dist/index.js +62 -17
- package/docs/USAGE.md +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -306,9 +306,11 @@ loops templates create-workflow custom-report \
|
|
|
306
306
|
Use `--source builtin`, `--source custom`, or `--source all` on
|
|
307
307
|
`list`, `show`, `render`, and `create-workflow` when automation needs an
|
|
308
308
|
explicit source. Custom template ids and names cannot override built-ins.
|
|
309
|
-
Custom templates fail closed for `danger-full-access
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
Custom templates fail closed for `danger-full-access`, dangerous passthrough
|
|
310
|
+
arguments, and implicit Codewith/Codex full-access defaults. If a custom
|
|
311
|
+
Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
|
|
312
|
+
`sandbox` to `workspace-write` or `read-only`. Use built-in templates with
|
|
313
|
+
explicit break-glass handling for emergency workflows that need full access.
|
|
312
314
|
|
|
313
315
|
For event-driven task automation, `loops events handle todos-task` reads a
|
|
314
316
|
Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
|
package/dist/cli/index.js
CHANGED
|
@@ -5908,7 +5908,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5908
5908
|
// package.json
|
|
5909
5909
|
var package_default = {
|
|
5910
5910
|
name: "@hasna/loops",
|
|
5911
|
-
version: "0.3.
|
|
5911
|
+
version: "0.3.51",
|
|
5912
5912
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5913
5913
|
type: "module",
|
|
5914
5914
|
main: "dist/index.js",
|
|
@@ -6204,6 +6204,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
|
6204
6204
|
var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
|
|
6205
6205
|
var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
6206
6206
|
var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
|
|
6207
|
+
var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
|
|
6208
|
+
"danger-full-access",
|
|
6209
|
+
"dangerously-bypass",
|
|
6210
|
+
"dangerously-skip"
|
|
6211
|
+
];
|
|
6207
6212
|
function compactJson(value) {
|
|
6208
6213
|
return JSON.stringify(value);
|
|
6209
6214
|
}
|
|
@@ -6526,6 +6531,13 @@ function validateCustomTemplateId(id, label) {
|
|
|
6526
6531
|
throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
|
|
6527
6532
|
}
|
|
6528
6533
|
}
|
|
6534
|
+
function optionalTemplateBoolean(value, label) {
|
|
6535
|
+
if (value === undefined)
|
|
6536
|
+
return;
|
|
6537
|
+
if (typeof value !== "boolean")
|
|
6538
|
+
throw new Error(`${label} must be a boolean`);
|
|
6539
|
+
return value;
|
|
6540
|
+
}
|
|
6529
6541
|
function validateCustomTemplateVariables(value, label) {
|
|
6530
6542
|
if (value === undefined)
|
|
6531
6543
|
return [];
|
|
@@ -6548,32 +6560,57 @@ function validateCustomTemplateVariables(value, label) {
|
|
|
6548
6560
|
if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
|
|
6549
6561
|
throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
|
|
6550
6562
|
}
|
|
6551
|
-
if (defaultValue
|
|
6552
|
-
throw new Error(`${entryLabel}.default cannot
|
|
6563
|
+
if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
|
|
6564
|
+
throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
|
|
6553
6565
|
}
|
|
6554
6566
|
return {
|
|
6555
6567
|
name,
|
|
6556
6568
|
description,
|
|
6557
|
-
required: entry.required
|
|
6569
|
+
required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
|
|
6558
6570
|
default: defaultValue,
|
|
6559
6571
|
type
|
|
6560
6572
|
};
|
|
6561
6573
|
});
|
|
6562
6574
|
}
|
|
6563
|
-
function
|
|
6575
|
+
function hasDangerousArg(value) {
|
|
6576
|
+
return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
|
|
6577
|
+
}
|
|
6578
|
+
function assertNoDangerousCustomTemplateScalars(value, label) {
|
|
6579
|
+
if (typeof value === "string") {
|
|
6580
|
+
if (hasDangerousArg(value)) {
|
|
6581
|
+
throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
|
|
6582
|
+
}
|
|
6583
|
+
return;
|
|
6584
|
+
}
|
|
6564
6585
|
if (!value || typeof value !== "object")
|
|
6565
6586
|
return;
|
|
6566
6587
|
if (Array.isArray(value)) {
|
|
6567
|
-
value.forEach((entry, index) =>
|
|
6588
|
+
value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
|
|
6568
6589
|
return;
|
|
6569
6590
|
}
|
|
6570
6591
|
for (const [key, entry] of Object.entries(value)) {
|
|
6571
|
-
|
|
6572
|
-
throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
|
|
6573
|
-
}
|
|
6574
|
-
assertNoDangerFullAccess(entry, `${label}.${key}`);
|
|
6592
|
+
assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
|
|
6575
6593
|
}
|
|
6576
6594
|
}
|
|
6595
|
+
function assertNoImplicitDangerFullAccess(value, label) {
|
|
6596
|
+
if (!value || typeof value !== "object")
|
|
6597
|
+
return;
|
|
6598
|
+
if (Array.isArray(value)) {
|
|
6599
|
+
value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
|
|
6600
|
+
return;
|
|
6601
|
+
}
|
|
6602
|
+
const object = value;
|
|
6603
|
+
if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
|
|
6604
|
+
throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
|
|
6605
|
+
}
|
|
6606
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
6607
|
+
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
function assertCustomTemplateSafety(value, label) {
|
|
6611
|
+
assertNoDangerousCustomTemplateScalars(value, label);
|
|
6612
|
+
assertNoImplicitDangerFullAccess(value, label);
|
|
6613
|
+
}
|
|
6577
6614
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
6578
6615
|
assertRecord(value, sourcePath);
|
|
6579
6616
|
const id = assertTemplateString(value.id, `${sourcePath}.id`);
|
|
@@ -6585,7 +6622,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
|
6585
6622
|
if (value.workflow === undefined)
|
|
6586
6623
|
throw new Error(`${sourcePath}.workflow is required`);
|
|
6587
6624
|
assertRecord(value.workflow, `${sourcePath}.workflow`);
|
|
6588
|
-
|
|
6625
|
+
assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
|
|
6589
6626
|
return { id, name, description, kind, variables, workflow: value.workflow };
|
|
6590
6627
|
}
|
|
6591
6628
|
function customTemplateSummary(definition, sourcePath) {
|
|
@@ -6626,11 +6663,11 @@ function assertNoTemplateCollisions(entries) {
|
|
|
6626
6663
|
}
|
|
6627
6664
|
}
|
|
6628
6665
|
}
|
|
6629
|
-
function
|
|
6666
|
+
function loadCustomLoopTemplatesRaw() {
|
|
6630
6667
|
const dir = customLoopTemplatesDir();
|
|
6631
6668
|
if (!existsSync4(dir))
|
|
6632
6669
|
return [];
|
|
6633
|
-
|
|
6670
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
6634
6671
|
const file = join5(dir, entry.name);
|
|
6635
6672
|
if (entry.isSymbolicLink())
|
|
6636
6673
|
throw new Error(`refusing symlinked custom template file: ${file}`);
|
|
@@ -6638,6 +6675,9 @@ function loadCustomLoopTemplates() {
|
|
|
6638
6675
|
throw new Error(`custom template registry entry is not a regular file: ${file}`);
|
|
6639
6676
|
return readCustomTemplateFile(file);
|
|
6640
6677
|
});
|
|
6678
|
+
}
|
|
6679
|
+
function loadCustomLoopTemplates() {
|
|
6680
|
+
const entries = loadCustomLoopTemplatesRaw();
|
|
6641
6681
|
assertNoTemplateCollisions(entries);
|
|
6642
6682
|
return entries;
|
|
6643
6683
|
}
|
|
@@ -6732,21 +6772,26 @@ function renderCustomTemplateNode(value, values, templateId) {
|
|
|
6732
6772
|
function renderCustomLoopTemplate(entry, values) {
|
|
6733
6773
|
const renderedValues = customTemplateValues(entry.definition, values);
|
|
6734
6774
|
const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
|
|
6735
|
-
|
|
6736
|
-
|
|
6775
|
+
assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
|
|
6776
|
+
const workflow = workflowBodyFromJson(rendered);
|
|
6777
|
+
assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
|
|
6778
|
+
return workflow;
|
|
6737
6779
|
}
|
|
6738
6780
|
function validateCustomLoopTemplateFile(file) {
|
|
6739
|
-
const
|
|
6740
|
-
|
|
6781
|
+
const source = resolve(file);
|
|
6782
|
+
const entry = readCustomTemplateFile(source);
|
|
6783
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
|
|
6784
|
+
assertNoTemplateCollisions([...existing, entry]);
|
|
6741
6785
|
return structuredClone(entry.summary);
|
|
6742
6786
|
}
|
|
6743
6787
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
6744
6788
|
const source = resolve(file);
|
|
6745
6789
|
const entry = readCustomTemplateFile(source);
|
|
6746
|
-
assertNoTemplateCollisions([entry]);
|
|
6747
6790
|
const dir = ensureCustomLoopTemplatesDir();
|
|
6748
6791
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
6749
6792
|
const replaced = existsSync4(destination);
|
|
6793
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
|
|
6794
|
+
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
6750
6795
|
if (replaced) {
|
|
6751
6796
|
const stat = lstatSync(destination);
|
|
6752
6797
|
if (!stat.isFile() || stat.isSymbolicLink())
|
package/dist/daemon/index.js
CHANGED
|
@@ -5240,7 +5240,7 @@ function enableStartup(result) {
|
|
|
5240
5240
|
// package.json
|
|
5241
5241
|
var package_default = {
|
|
5242
5242
|
name: "@hasna/loops",
|
|
5243
|
-
version: "0.3.
|
|
5243
|
+
version: "0.3.51",
|
|
5244
5244
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5245
5245
|
type: "module",
|
|
5246
5246
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -5183,6 +5183,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
|
5183
5183
|
var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
|
|
5184
5184
|
var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
5185
5185
|
var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
|
|
5186
|
+
var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
|
|
5187
|
+
"danger-full-access",
|
|
5188
|
+
"dangerously-bypass",
|
|
5189
|
+
"dangerously-skip"
|
|
5190
|
+
];
|
|
5186
5191
|
function compactJson(value) {
|
|
5187
5192
|
return JSON.stringify(value);
|
|
5188
5193
|
}
|
|
@@ -5505,6 +5510,13 @@ function validateCustomTemplateId(id, label) {
|
|
|
5505
5510
|
throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
|
|
5506
5511
|
}
|
|
5507
5512
|
}
|
|
5513
|
+
function optionalTemplateBoolean(value, label) {
|
|
5514
|
+
if (value === undefined)
|
|
5515
|
+
return;
|
|
5516
|
+
if (typeof value !== "boolean")
|
|
5517
|
+
throw new Error(`${label} must be a boolean`);
|
|
5518
|
+
return value;
|
|
5519
|
+
}
|
|
5508
5520
|
function validateCustomTemplateVariables(value, label) {
|
|
5509
5521
|
if (value === undefined)
|
|
5510
5522
|
return [];
|
|
@@ -5527,32 +5539,57 @@ function validateCustomTemplateVariables(value, label) {
|
|
|
5527
5539
|
if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
|
|
5528
5540
|
throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
|
|
5529
5541
|
}
|
|
5530
|
-
if (defaultValue
|
|
5531
|
-
throw new Error(`${entryLabel}.default cannot
|
|
5542
|
+
if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
|
|
5543
|
+
throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
|
|
5532
5544
|
}
|
|
5533
5545
|
return {
|
|
5534
5546
|
name,
|
|
5535
5547
|
description,
|
|
5536
|
-
required: entry.required
|
|
5548
|
+
required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
|
|
5537
5549
|
default: defaultValue,
|
|
5538
5550
|
type
|
|
5539
5551
|
};
|
|
5540
5552
|
});
|
|
5541
5553
|
}
|
|
5542
|
-
function
|
|
5554
|
+
function hasDangerousArg(value) {
|
|
5555
|
+
return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
|
|
5556
|
+
}
|
|
5557
|
+
function assertNoDangerousCustomTemplateScalars(value, label) {
|
|
5558
|
+
if (typeof value === "string") {
|
|
5559
|
+
if (hasDangerousArg(value)) {
|
|
5560
|
+
throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
|
|
5561
|
+
}
|
|
5562
|
+
return;
|
|
5563
|
+
}
|
|
5543
5564
|
if (!value || typeof value !== "object")
|
|
5544
5565
|
return;
|
|
5545
5566
|
if (Array.isArray(value)) {
|
|
5546
|
-
value.forEach((entry, index) =>
|
|
5567
|
+
value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
|
|
5547
5568
|
return;
|
|
5548
5569
|
}
|
|
5549
5570
|
for (const [key, entry] of Object.entries(value)) {
|
|
5550
|
-
|
|
5551
|
-
throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
|
|
5552
|
-
}
|
|
5553
|
-
assertNoDangerFullAccess(entry, `${label}.${key}`);
|
|
5571
|
+
assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
|
|
5554
5572
|
}
|
|
5555
5573
|
}
|
|
5574
|
+
function assertNoImplicitDangerFullAccess(value, label) {
|
|
5575
|
+
if (!value || typeof value !== "object")
|
|
5576
|
+
return;
|
|
5577
|
+
if (Array.isArray(value)) {
|
|
5578
|
+
value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5581
|
+
const object = value;
|
|
5582
|
+
if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
|
|
5583
|
+
throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
|
|
5584
|
+
}
|
|
5585
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
5586
|
+
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
function assertCustomTemplateSafety(value, label) {
|
|
5590
|
+
assertNoDangerousCustomTemplateScalars(value, label);
|
|
5591
|
+
assertNoImplicitDangerFullAccess(value, label);
|
|
5592
|
+
}
|
|
5556
5593
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
5557
5594
|
assertRecord(value, sourcePath);
|
|
5558
5595
|
const id = assertTemplateString(value.id, `${sourcePath}.id`);
|
|
@@ -5564,7 +5601,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
|
5564
5601
|
if (value.workflow === undefined)
|
|
5565
5602
|
throw new Error(`${sourcePath}.workflow is required`);
|
|
5566
5603
|
assertRecord(value.workflow, `${sourcePath}.workflow`);
|
|
5567
|
-
|
|
5604
|
+
assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
|
|
5568
5605
|
return { id, name, description, kind, variables, workflow: value.workflow };
|
|
5569
5606
|
}
|
|
5570
5607
|
function customTemplateSummary(definition, sourcePath) {
|
|
@@ -5605,11 +5642,11 @@ function assertNoTemplateCollisions(entries) {
|
|
|
5605
5642
|
}
|
|
5606
5643
|
}
|
|
5607
5644
|
}
|
|
5608
|
-
function
|
|
5645
|
+
function loadCustomLoopTemplatesRaw() {
|
|
5609
5646
|
const dir = customLoopTemplatesDir();
|
|
5610
5647
|
if (!existsSync3(dir))
|
|
5611
5648
|
return [];
|
|
5612
|
-
|
|
5649
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
5613
5650
|
const file = join5(dir, entry.name);
|
|
5614
5651
|
if (entry.isSymbolicLink())
|
|
5615
5652
|
throw new Error(`refusing symlinked custom template file: ${file}`);
|
|
@@ -5617,6 +5654,9 @@ function loadCustomLoopTemplates() {
|
|
|
5617
5654
|
throw new Error(`custom template registry entry is not a regular file: ${file}`);
|
|
5618
5655
|
return readCustomTemplateFile(file);
|
|
5619
5656
|
});
|
|
5657
|
+
}
|
|
5658
|
+
function loadCustomLoopTemplates() {
|
|
5659
|
+
const entries = loadCustomLoopTemplatesRaw();
|
|
5620
5660
|
assertNoTemplateCollisions(entries);
|
|
5621
5661
|
return entries;
|
|
5622
5662
|
}
|
|
@@ -5711,21 +5751,26 @@ function renderCustomTemplateNode(value, values, templateId) {
|
|
|
5711
5751
|
function renderCustomLoopTemplate(entry, values) {
|
|
5712
5752
|
const renderedValues = customTemplateValues(entry.definition, values);
|
|
5713
5753
|
const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
|
|
5714
|
-
|
|
5715
|
-
|
|
5754
|
+
assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
|
|
5755
|
+
const workflow = workflowBodyFromJson(rendered);
|
|
5756
|
+
assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
|
|
5757
|
+
return workflow;
|
|
5716
5758
|
}
|
|
5717
5759
|
function validateCustomLoopTemplateFile(file) {
|
|
5718
|
-
const
|
|
5719
|
-
|
|
5760
|
+
const source = resolve(file);
|
|
5761
|
+
const entry = readCustomTemplateFile(source);
|
|
5762
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
|
|
5763
|
+
assertNoTemplateCollisions([...existing, entry]);
|
|
5720
5764
|
return structuredClone(entry.summary);
|
|
5721
5765
|
}
|
|
5722
5766
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
5723
5767
|
const source = resolve(file);
|
|
5724
5768
|
const entry = readCustomTemplateFile(source);
|
|
5725
|
-
assertNoTemplateCollisions([entry]);
|
|
5726
5769
|
const dir = ensureCustomLoopTemplatesDir();
|
|
5727
5770
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
5728
5771
|
const replaced = existsSync3(destination);
|
|
5772
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
|
|
5773
|
+
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
5729
5774
|
if (replaced) {
|
|
5730
5775
|
const stat = lstatSync(destination);
|
|
5731
5776
|
if (!stat.isFile() || stat.isSymbolicLink())
|
package/docs/USAGE.md
CHANGED
|
@@ -309,9 +309,11 @@ loops templates create-workflow custom-report \
|
|
|
309
309
|
Use `--source builtin`, `--source custom`, or `--source all` on
|
|
310
310
|
`list`, `show`, `render`, and `create-workflow` when automation needs an
|
|
311
311
|
explicit source. Custom template ids and names cannot override built-ins.
|
|
312
|
-
Custom templates fail closed for `danger-full-access
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
Custom templates fail closed for `danger-full-access`, dangerous passthrough
|
|
313
|
+
arguments, and implicit Codewith/Codex full-access defaults. If a custom
|
|
314
|
+
Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
|
|
315
|
+
`sandbox` to `workspace-write` or `read-only`. Use built-in templates with
|
|
316
|
+
explicit break-glass handling for emergency workflows that need full access.
|
|
315
317
|
|
|
316
318
|
Repo-mutating task/event routes should set `worktreeMode=required` so the
|
|
317
319
|
workflow fails fast instead of falling back to the main checkout. When
|
package/package.json
CHANGED